// app.jsx — Team Task Hub · real app wired to API
const { useState, useEffect, useCallback, useRef } = React;

// ---- API layer ----
// API base ตั้งจาก window.API_BASE ใน index.html (config เดียว) → ชี้ข้ามเครื่องไป backend คุณเอก
// ทุก request ต้องใช้ credentials:"include" (auth = cookie ข้าม subdomain .newverse.cloud)
const API = (typeof window!=="undefined" && window.API_BASE) || ""; // "" = same-origin (dev fallback)
function token(){ return localStorage.getItem("th_token") || ""; }
function setToken(t){ if(t) localStorage.setItem("th_token",t); else localStorage.removeItem("th_token"); }
function getUser(){ try{ return JSON.parse(localStorage.getItem("th_user")||"null"); }catch(e){ return null; } }
function setUser(u){ u ? localStorage.setItem("th_user",JSON.stringify(u)) : localStorage.removeItem("th_user"); }
// ---- impersonate (superadmin ดูในมุมมองคนอื่น) ----
function isImp(){ return !!localStorage.getItem("th_real_token"); }
function realUser(){ try{ return JSON.parse(localStorage.getItem("th_real_user")||"null"); }catch(e){ return null; } }
function stopImp(){ const rt=localStorage.getItem("th_real_token"), ru=localStorage.getItem("th_real_user");
  if(rt) localStorage.setItem("th_token",rt); if(ru) localStorage.setItem("th_user",ru);
  localStorage.removeItem("th_real_token"); localStorage.removeItem("th_real_user"); }

async function api(method, path, body){
  const h = { "Content-Type":"application/json" };
  if(token()) h["Authorization"] = "Bearer "+token();
  const res = await fetch(API+path, { method, headers:h, credentials:"include", body: body?JSON.stringify(body):undefined });
  if(res.status===401){ setToken(null); setUser(null); location.reload(); return; }
  if(!res.ok) throw new Error((await res.text()).slice(0,200));
  return res.status===204 ? null : res.json();
}

// ---- pipeline mapping (API key → design token key) ----
const PIPE_MAP = { bot_dev:"bot", long_video:"long", short_video:"short", graphic:"graphic", page_content:"page", head_review:"review", ops:"ops", strategy:"strategy" };
function pipeTok(apiKey){ return PIPELINES[PIPE_MAP[apiKey]] || { color:"#9B93B5", soft:"rgba(155,147,181,0.14)", label:apiKey, short:apiKey }; }
function statusTok(s){ return STATUS[s] || { bg:"rgba(155,147,181,0.14)", fg:"#9B93B5", dot:"#9B93B5", label:s }; }
const STATUS_OPTIONS = ["idea","received","progress","editing","submitted","review","revision","approved","to_publish","done","published","blocked"];

// ---- small UI ----
function Badge({children, bg, fg}){ return <span style={{background:bg,color:fg,padding:"3px 9px",borderRadius:999,fontSize:12,fontWeight:600,whiteSpace:"nowrap"}}>{children}</span>; }

function StatusPill({s}){ const t=statusTok(s); return <span style={{display:"inline-flex",alignItems:"center",gap:6,background:t.bg,color:t.fg,padding:"3px 10px",borderRadius:999,fontSize:12,fontWeight:600}}><span style={{width:7,height:7,borderRadius:99,background:t.dot}}/>{t.label}</span>; }

// ---- Toast (non-blocking · แทน alert · รองรับ undo) ----
let _toastSeq=0; const _toastSubs=new Set();
function toast(msg,opts={}){ const t={id:++_toastSeq,msg:(""+msg).slice(0,200),kind:opts.kind||"info",action:opts.action,actionLabel:opts.actionLabel,ttl:opts.ttl||(opts.action?6000:3200)}; _toastSubs.forEach(fn=>fn(t)); return t.id; }
toast.error=(m)=>toast(m,{kind:"error"}); toast.ok=(m,o={})=>toast(m,{...o,kind:"ok"});
function ToastHost(){
  const [items,setItems]=useState([]);
  useEffect(()=>{ const sub=(t)=>{ setItems(xs=>[...xs,t]); setTimeout(()=>setItems(xs=>xs.filter(x=>x.id!==t.id)),t.ttl); }; _toastSubs.add(sub); return ()=>_toastSubs.delete(sub); },[]);
  const close=(id)=>setItems(xs=>xs.filter(x=>x.id!==id));
  return <div style={{position:"fixed",left:0,right:0,bottom:24,display:"flex",flexDirection:"column",alignItems:"center",gap:8,zIndex:9000,pointerEvents:"none",padding:"0 16px"}}>
    {items.map(t=>{ const c=t.kind==="error"?STATUS.blocked.fg:t.kind==="ok"?STATUS.done.fg:COLORS.accentBright;
      return <div key={t.id} style={{pointerEvents:"auto",display:"flex",alignItems:"center",gap:12,maxWidth:440,width:"100%",background:COLORS.bgElev,border:`1px solid var(--th-hairline-str)`,borderLeft:`3px solid ${c}`,borderRadius:12,padding:"11px 14px",boxShadow:"var(--th-shadow-md)",color:COLORS.ink,fontSize:13.5,fontWeight:500}}>
        <span style={{flex:1,lineHeight:1.4,wordBreak:"break-word"}}>{t.msg}</span>
        {t.action && <button onClick={()=>{try{t.action();}catch(_){}close(t.id);}} style={{background:`color-mix(in srgb, ${c} 14%, transparent)`,color:c,border:`1px solid color-mix(in srgb, ${c} 38%, transparent)`,borderRadius:999,padding:"5px 13px",fontWeight:700,fontSize:12.5,cursor:"pointer",whiteSpace:"nowrap"}}>{t.actionLabel||"เลิกทำ"}</button>}
        <button onClick={()=>close(t.id)} style={{background:"transparent",color:COLORS.inkMuted,fontSize:16,lineHeight:1,cursor:"pointer"}}>×</button>
      </div>; })}
  </div>;
}
// ปิด modal ด้วย Esc (desktop)
function useEscClose(onClose){
  useEffect(()=>{ const h=e=>{ if(e.key==="Escape"){ e.stopPropagation(); onClose&&onClose(); } }; window.addEventListener("keydown",h); return ()=>window.removeEventListener("keydown",h); },[onClose]);
}

// ---- Confirm dialog (promise-based · กันลบ/อนุมัติ/เผยแพร่พลาด + กันสวมบทแล้วแก้พลาด) ----
let _confirmSub=null;
function confirmAsk(opts){
  return new Promise(resolve=>{
    if(!_confirmSub){ resolve(window.confirm(((opts.title||"ยืนยัน")+"\n"+(opts.message||"")).trim())); return; }
    _confirmSub({...opts,_resolve:resolve});
  });
}
// gate: คืน true ถ้าผ่าน (กดยืนยัน หรือไม่ต้องถาม) · false ถ้ายกเลิก
// always=ถามเสมอ (อนุมัติ/เผยแพร่/ตีกลับ) · danger=อันตราย+ถามเสมอ (ลบ/ยกเลิก) · นอกนั้นถามเฉพาะตอนสวมบท (isImp)
async function gate(o){
  o=o||{}; const imp=isImp();
  if(!o.always && !o.danger && !imp) return true;
  let impLine=""; if(imp){ const nm=(getUser()||{}).name||"คนอื่น"; impLine=`⚠️ คุณกำลังสวมบทเป็น ${nm} — การกระทำนี้จะถูกบันทึกในนามนั้น\n\n`; }
  return await confirmAsk({ title:o.title||"ยืนยัน", message:impLine+(o.body||"ดำเนินการต่อหรือไม่?"),
    danger:!!o.danger, confirmLabel:o.confirmLabel||(o.danger?"ยืนยันลบ":"ยืนยัน"), requireText:o.requireText });
}
function ConfirmHost(){
  const [dlg,setDlg]=useState(null); const [typed,setTyped]=useState("");
  useEffect(()=>{ _confirmSub=(d)=>{ setTyped(""); setDlg(d); }; return ()=>{ _confirmSub=null; }; },[]);
  if(!dlg) return null;
  const fin=(v)=>{ const r=dlg._resolve; setDlg(null); setTyped(""); r&&r(v); };
  const danger=!!dlg.danger, c=danger?"var(--th-st-red-fg)":COLORS.accentBright;
  const need=dlg.requireText, okOff=need && typed.trim()!==(""+need).trim();
  return <div onClick={()=>fin(false)} style={{position:"fixed",inset:0,background:"var(--th-overlay-strong)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:9500,padding:20}}>
    <div onClick={e=>e.stopPropagation()} style={{width:"100%",maxWidth:400,background:COLORS.bgElev,borderRadius:18,padding:22,border:`1px solid ${danger?"color-mix(in srgb, var(--th-st-red-fg) 35%, transparent)":COLORS.hairline}`,boxShadow:"var(--th-shadow-lg)"}}>
      <div style={{fontSize:17,fontWeight:800,color:danger?"var(--th-st-red-fg)":COLORS.inkBright,marginBottom:9,display:"flex",alignItems:"center",gap:8}}>{danger&&<span>🗑</span>}{dlg.title||"ยืนยัน"}</div>
      {dlg.message && <div style={{fontSize:13.5,color:COLORS.inkMuted,lineHeight:1.55,marginBottom:need?14:18,whiteSpace:"pre-wrap"}}>{dlg.message}</div>}
      {need && <input autoFocus value={typed} onChange={e=>setTyped(e.target.value)} onKeyDown={e=>{if(e.key==="Enter"&&!okOff)fin(true);}} placeholder={`พิมพ์ "${need}" เพื่อยืนยัน`} style={{width:"100%",padding:"11px 13px",borderRadius:11,background:COLORS.card,border:`1px solid ${COLORS.hairline}`,color:COLORS.ink,fontSize:14,marginBottom:16}}/>}
      <div style={{display:"flex",gap:10}}>
        <button onClick={()=>fin(false)} style={{flex:1,padding:12,borderRadius:12,background:COLORS.card,color:COLORS.ink,border:`1px solid ${COLORS.hairline}`,fontWeight:600,fontSize:14,cursor:"pointer"}}>{dlg.cancelLabel||"ยกเลิก"}</button>
        <button disabled={okOff} onClick={()=>fin(true)} style={{flex:1,padding:12,borderRadius:12,background:okOff?COLORS.card:(danger?"var(--th-st-red-dot)":COLORS.accent),color:okOff?COLORS.inkMuted:"var(--th-on-accent)",border:`1px solid ${okOff?COLORS.hairline:(danger?"var(--th-st-red-dot)":COLORS.accent)}`,fontWeight:700,fontSize:14,cursor:okOff?"default":"pointer",opacity:okOff?.55:1}}>{dlg.confirmLabel||"ยืนยัน"}</button>
      </div>
    </div>
  </div>;
}

// ---- Login ----
function Login({onLogin}){
  const [email,setEmail]=useState(""); const [pw,setPw]=useState(""); const [err,setErr]=useState(""); const [busy,setBusy]=useState(false); const [pwMode,setPwMode]=useState(false);
  const gbtn=useRef(null);
  async function submit(e){ e.preventDefault(); setBusy(true); setErr("");
    try{ const r=await api("POST","/api/auth/login",{email,password:pw}); setToken(r.token); setUser(r.user); onLogin(r.user); }
    catch(e){ setErr("อีเมลหรือรหัสผ่านไม่ถูกต้อง"); } finally{ setBusy(false); } }
  async function googleLogin(credential){ setBusy(true); setErr("");
    try{ const r=await api("POST","/api/auth/google",{credential}); setToken(r.token); setUser(r.user); onLogin(r.user); }
    catch(e){ setErr("ล็อกอิน Google ไม่สำเร็จ · อีเมลนี้อาจยังไม่มีในระบบ Team Hub"); setBusy(false); } }
  useEffect(()=>{ let n=0; const CID=(typeof window!=="undefined"&&window.GOOGLE_CLIENT_ID)||"";
    const init=()=>{ const g=window.google&&window.google.accounts&&window.google.accounts.id;
      if(g&&gbtn.current&&CID){ try{ window.google.accounts.id.initialize({client_id:CID,callback:(resp)=>googleLogin(resp.credential)});
        window.google.accounts.id.renderButton(gbtn.current,{theme:"filled_black",size:"large",shape:"pill",text:"signin_with",width:300,logo_alignment:"center"}); }catch(e){} }
      else if(n++<50){ setTimeout(init,150); } };
    init(); },[]);
  const inp={width:"100%",padding:"14px 16px",borderRadius:14,background:COLORS.card,border:`1px solid ${COLORS.hairline}`,color:COLORS.ink,fontSize:15,marginBottom:12};
  return (
    <div style={{minHeight:"100vh",background:COLORS.bgGrad,display:"flex",alignItems:"center",justifyContent:"center",padding:24,position:"relative"}}>
      <div style={{position:"absolute",top:18,right:18}}><ThemeToggle compact/></div>
      <div style={{width:"100%",maxWidth:380}}>
        <div style={{textAlign:"center",marginBottom:28}}>
          <div style={{fontSize:13,letterSpacing:3,color:COLORS.accentBright,fontWeight:700}}>NEWVERSE</div>
          <div style={{fontSize:26,fontWeight:700,marginTop:6}}>Team Task Hub</div>
          <div style={{color:COLORS.inkMuted,fontSize:14,marginTop:6}}>เข้าสู่ระบบด้วยบัญชี Google ของทีม</div>
        </div>
        <div ref={gbtn} style={{display:"flex",justifyContent:"center",marginBottom:14,minHeight:44}}/>
        {err && <div style={{color:"var(--th-st-red-fg)",fontSize:13,marginBottom:12,textAlign:"center"}}>{err}</div>}
        {busy && <div style={{color:COLORS.inkMuted,fontSize:12.5,textAlign:"center",marginBottom:8}}>กำลังเข้าสู่ระบบ…</div>}
        {!pwMode
          ? <div onClick={()=>setPwMode(true)} style={{textAlign:"center",color:COLORS.inkMuted,fontSize:12,marginTop:18,cursor:"pointer"}}>เข้าด้วยอีเมล/รหัสผ่าน</div>
          : <form onSubmit={submit} style={{marginTop:14}}>
              <input style={inp} placeholder="อีเมล" value={email} onChange={e=>setEmail(e.target.value)} autoCapitalize="off"/>
              <input style={inp} type="password" placeholder="รหัสผ่าน" value={pw} onChange={e=>setPw(e.target.value)}/>
              <button disabled={busy} style={{width:"100%",padding:"15px",borderRadius:14,background:COLORS.accent,color:"#fff",fontSize:16,fontWeight:700,opacity:busy?.6:1}}>{busy?"กำลังเข้า...":"เข้าสู่ระบบ"}</button>
            </form>}
      </div>
    </div>
  );
}

// ---- Task card ----
const THMON=["ม.ค.","ก.พ.","มี.ค.","เม.ย.","พ.ค.","มิ.ย.","ก.ค.","ส.ค.","ก.ย.","ต.ค.","พ.ย.","ธ.ค."];
const DONE_STATES=["done","published","approved","deployed"];
const TERMINAL_ST=["done","published","approved","deployed","cancelled"];   // ปิดงานแล้ว (เสร็จ/ยกเลิก)
// due → {text:"5 มิ.ย.", color} · แดงเมื่อเลยกำหนด · เหลืองเมื่อเหลือ ≤2 วัน (ยกเว้นงานปิดแล้ว)
function dueInfo(due,status){
  if(!due) return null;
  const m=(""+due).match(/^(\d{4})-(\d{2})-(\d{2})/); if(!m) return {text:due,color:COLORS.inkMuted};
  const d=new Date(+m[1],+m[2]-1,+m[3]); const today=new Date(); today.setHours(0,0,0,0);
  const days=Math.round((d-today)/86400000), terminal=["done","published","approved","deployed"].includes(status);
  let color=COLORS.inkMuted;
  if(!terminal){ if(days<0) color="var(--th-st-red-fg)"; else if(days<=2) color="var(--th-st-orange-fg)"; }
  return {text:(days<0&&!terminal?"เลย ":"")+`${+m[3]} ${THMON[+m[2]-1]}`, color, late:days<0&&!terminal};
}
function TaskCard({t, onClick, onToggleDone, focused, picked, onPick, selMode, onTagClick, onPatch}){
  const [editing,setEditing]=useState(false); const [ev,setEv]=useState("");
  const cycle=()=>{ const o=["P0","P1","P2","P3"]; onPatch&&onPatch(t,{priority:o[(o.indexOf(t.priority)+1)%4]}); };
  const saveTitle=()=>{ const v=ev.trim(); if(v&&v!==t.title) onPatch&&onPatch(t,{title:v}); setEditing(false); };
  const p=pipeTok(t.pipeline_key); const pr=PRIORITY[t.priority]||PRIORITY.P2;
  const di=dueInfo(t.due,t.status); const done=DONE_STATES.includes(t.status); const terminal=TERMINAL_ST.includes(t.status);
  const tags=(t.tags||"").split(",").map(s=>s.trim()).filter(Boolean);
  let clT=0,clD=0; try{const c=JSON.parse(t.checklist||"[]"); clT=c.length; clD=c.filter(x=>x.done).length;}catch(e){}
  const docN=(t.attach_count||0)+(t.link_count||0);   // เอกสาร/ไฟล์/ลิงก์แนบ → badge เฉพาะตอนมี
  return (
    <div onClick={selMode?(()=>onPick&&onPick(t)):onClick} data-tid={t.id} style={{background:picked?COLORS.accentSoft:COLORS.card,border:`1px solid ${picked||focused?COLORS.accentEdge:COLORS.hairline}`,borderRadius:16,padding:14,marginBottom:10,cursor:"pointer",opacity:terminal?0.6:1,transition:"opacity .2s,border .15s,background .15s",boxShadow:focused?`0 0 0 1px ${COLORS.accentEdge}`:"none"}}>
      <div style={{display:"flex",justifyContent:"space-between",gap:8,marginBottom:8,alignItems:"center"}}>
        <div style={{display:"flex",gap:6,alignItems:"center"}}><Badge bg={p.soft} fg={p.color}>{p.short}</Badge>{t.recurrence&&<span title={RECUR_LABEL[t.recurrence]||"ทำซ้ำ"} style={{fontSize:12}}>🔁</span>}</div>
        <span onClick={onPatch?(e=>{e.stopPropagation();cycle();}):undefined} title={onPatch?"คลิกเปลี่ยนความสำคัญ":""} style={{cursor:onPatch?"pointer":"default"}}><Badge bg={pr.bg} fg={pr.color}>{pr.label}</Badge></span>
      </div>
      <div style={{display:"flex",gap:10,alignItems:"flex-start",marginBottom:10}}>
        {selMode ? <span style={{flexShrink:0,marginTop:1,width:20,height:20,borderRadius:6,border:`2px solid ${picked?COLORS.accentBright:COLORS.hairlineStr}`,background:picked?COLORS.accentBright:"transparent",color:"#0A0614",fontSize:13,fontWeight:900,display:"flex",alignItems:"center",justifyContent:"center"}}>{picked?"✓":""}</span>
        : onToggleDone && <button onClick={e=>{e.stopPropagation();onToggleDone(t);}} title={done?"เลิกทำเสร็จ":"ทำเสร็จ"}
          style={{flexShrink:0,marginTop:1,width:20,height:20,borderRadius:999,cursor:"pointer",padding:0,
            border:`2px solid ${done?STATUS.done.dot:COLORS.hairlineStr}`,background:done?STATUS.done.dot:"transparent",
            color:"#0A0614",fontSize:12,fontWeight:900,lineHeight:1,display:"flex",alignItems:"center",justifyContent:"center",transition:"transform .12s, background .15s",transform:done?"scale(1.05)":"scale(1)"}}>{done?"✓":""}</button>}
        {editing
          ? <input autoFocus value={ev} onClick={e=>e.stopPropagation()} onChange={e=>setEv(e.target.value)}
              onKeyDown={e=>{ if(e.key==="Enter"){e.preventDefault();saveTitle();} else if(e.key==="Escape"){setEditing(false);} }} onBlur={saveTitle}
              style={{flex:1,fontSize:15,fontWeight:600,padding:"3px 6px",borderRadius:7,border:`1px solid ${COLORS.accentEdge}`,background:COLORS.bgElev,color:COLORS.ink,boxSizing:"border-box"}}/>
          : <div onDoubleClick={onPatch?(e=>{e.stopPropagation();setEv(t.title);setEditing(true);}):undefined} title={onPatch?"ดับเบิลคลิกแก้ชื่อ":""}
              style={{flex:1,fontSize:15,fontWeight:600,lineHeight:1.4,textDecoration:terminal?"line-through":"none",color:terminal?COLORS.inkMuted:COLORS.ink}}>{t.title}</div>}
      </div>
      {(clT>0||docN>0) && <div style={{display:"flex",gap:6,flexWrap:"wrap",alignItems:"center",marginBottom:9}}>
        {clT>0 && <span style={{fontSize:10.5,fontWeight:700,fontFamily:MONO_FONT,color:clD===clT?STATUS.done.fg:COLORS.inkMuted}}>☑ {clD}/{clT}</span>}
        {docN>0 && <span title={`มีเอกสาร/ไฟล์แนบ ${docN} รายการ`} style={{fontSize:10.5,fontWeight:700,fontFamily:MONO_FONT,color:COLORS.inkMuted,display:"inline-flex",alignItems:"center",gap:2}}>📎 {docN}</span>}
      </div>}
      <div style={{display:"flex",justifyContent:"space-between",alignItems:"center"}}>
        <StatusPill s={t.status}/>
        <span style={{color:COLORS.inkMuted,fontSize:12,display:"inline-flex",alignItems:"center",gap:5}}>{t.owner_name||"—"}{di&&<span style={{color:di.color,fontWeight:600}}>· 📅 {di.text}</span>}</span>
      </div>
    </div>
  );
}
// quick-add บรรทัดเดียว · พิมพ์ Enter สร้างทันที · รองรับ !P1 / วันนี้ / พรุ่งนี้ / YYYY-MM-DD
// แยก token จากชื่องาน → {title สะอาด, priority, due} · ใช้ทั้ง quick-add และฟอร์มเต็ม (พิมพ์ที่ไหนก็ parse)
function parseQuick(raw){
  let title=(raw||"").trim(), priority=null, due=null, recurrence=null;
  const fmt=d=>`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`;
  const pm=title.match(/!\s*(p[0-3])\b/i); if(pm){ priority=pm[1].toUpperCase(); title=title.replace(pm[0],""); }
  if(/ทุกวัน|รายวัน|daily/i.test(title)){ recurrence="daily"; title=title.replace(/ทุกวัน|รายวัน|daily/i,""); }
  else if(/ทุกสัปดาห์|รายสัปดาห์|weekly/i.test(title)){ recurrence="weekly"; title=title.replace(/ทุกสัปดาห์|รายสัปดาห์|weekly/i,""); }
  if(/มะรืนนี้|มะรืน/i.test(title)){ const d=new Date();d.setDate(d.getDate()+2);due=fmt(d);title=title.replace(/มะรืนนี้|มะรืน/i,""); }
  else if(/พรุ่งนี้|tomorrow/i.test(title)){ const d=new Date();d.setDate(d.getDate()+1);due=fmt(d);title=title.replace(/พรุ่งนี้|tomorrow/i,""); }
  else if(/วันนี้|today/i.test(title)){ due=fmt(new Date());title=title.replace(/วันนี้|today/i,""); }
  else if(/สัปดาห์หน้า|อาทิตย์หน้า|next week/i.test(title)){ const d=new Date();d.setDate(d.getDate()+7);due=fmt(d);title=title.replace(/สัปดาห์หน้า|อาทิตย์หน้า|next week/i,""); }
  const dm=title.match(/\b(\d{4}-\d{2}-\d{2})\b/); if(dm){ due=dm[1]; title=title.replace(dm[0],""); }
  title=title.replace(/\s{2,}/g," ").trim();
  return {title, priority, due, recurrence};
}
const RECUR_LABEL={daily:"🔁 ทุกวัน",weekly:"🔁 ทุกสัปดาห์"};
function QuickAdd({onAdd}){
  const [v,setV]=useState(""); const [busy,setBusy]=useState(false);
  const submit=async()=>{ const t=v.trim(); if(!t||busy)return; setBusy(true); setV(""); try{ await onAdd(t); }finally{ setBusy(false); } };
  return <div style={{display:"flex",alignItems:"center",gap:8,padding:"11px 13px",marginBottom:10,borderRadius:12,background:COLORS.card,border:`1px solid ${v.trim()?COLORS.accentEdge:COLORS.hairline}`}}>
    <span style={{color:COLORS.accentBright,fontSize:17,fontWeight:700,lineHeight:1}}>+</span>
    <input value={v} onChange={e=>setV(e.target.value)} onKeyDown={e=>{if(e.key==="Enter")submit();}}
      placeholder="เพิ่มงานเร็ว… เช่น ตัดต่อ EP1 !P1 พรุ่งนี้" style={{flex:1,minWidth:0,background:"transparent",border:"none",outline:"none",color:COLORS.ink,fontSize:14}}/>
    {v.trim() && <button onClick={submit} disabled={busy} style={{padding:"5px 13px",borderRadius:999,background:COLORS.accent,color:"#fff",fontWeight:700,fontSize:12.5,cursor:"pointer",whiteSpace:"nowrap"}}>เพิ่ม</button>}
  </div>;
}

// ---- Detail sheet ----
// render `extra` JSON (รายละเอียดงาน: done/todo/roles/domain ฯลฯ) ใน Detail modal
function ExtraView({raw}){
  if(!raw) return null;
  let d; try{ d=typeof raw==="string"?JSON.parse(raw):raw; }catch(e){ return null; }
  if(!d||typeof d!=="object"||Array.isArray(d)) return null;
  const keys=Object.keys(d); if(!keys.length) return null;
  const LBL={domain:"🌐 โดเมน",dev:"👨‍💻 ผู้พัฒนา",done:"✅ เสร็จแล้ว",todo:"⬜ เหลือทำ",progress:"✅ คืบหน้าแล้ว",platform:"📺 แพลตฟอร์ม",roles:"👥 หน้าที่ทีม",flow:"🔀 ขั้นตอน",note:"📝 โน้ต",stack:"⚙️ Stack",hosting:"🖥 Hosting",brokers:"🏦 โบรกเกอร์",goal:"🎯 เป้าหมาย",plan:"🗺 Action Plan",action_plan:"🗺 Action Plan",why:"💡 ทำไม impact สูง",tech:"⚙️ เทคนิค",effort:"⚖️ Effort",owner_detail:"👥 ใครทำ",expected:"📈 ผลคาดหวัง",steps:"🗺 ขั้นตอน",impact_tier:"⭐ ระดับ"};
  const card={marginBottom:14};   // เลิกกรอบกล่องนอก (กล่องในกล่อง) · ปล่อยเนื้อหาไหลใต้ stage bar
  const lab={color:COLORS.inkMuted,fontSize:11,fontWeight:700,marginBottom:3,fontFamily:MONO_FONT,letterSpacing:0.3};
  const itm={fontSize:14,color:COLORS.ink,padding:"2px 0"};
  const phc={background:COLORS.bg,border:`1px solid ${COLORS.hairline}`,borderRadius:10,padding:"10px 12px",marginBottom:8};
  function renderVal(k,v){
    if(Array.isArray(v)){
      if(v.length && typeof v[0]==="object")   // array ของ phase object → การ์ดแต่ละ phase
        return v.map((ph,i)=><div key={i} style={phc}>
          <div style={{fontSize:14,fontWeight:700,color:COLORS.ink,marginBottom:3}}>{ph.phase||ph.title}</div>
          {(ph.owner||ph.eta)&&<div style={{fontSize:12,color:COLORS.accentBright,marginBottom:5}}>{ph.owner?"👤 "+ph.owner:""}{ph.eta?" · ⏱ "+ph.eta:""}</div>}
          {(ph.steps||[]).map((s,j)=><div key={j} style={{fontSize:13,color:COLORS.ink,padding:"1px 0"}}>• {s}</div>)}
          {ph.done&&<div style={{fontSize:12,color:"#6EE5A0",marginTop:5}}>✓ เสร็จเมื่อ: {ph.done}</div>}
        </div>);
      const pre = k==="todo"?"⬜ ":(k==="done"||k==="progress")?"✅ ":"• ";
      return v.map((x,i)=><div key={i} style={itm}>{pre}{x}</div>);
    }
    if(v&&typeof v==="object")
      return Object.entries(v).map(([kk,vv])=><div key={kk} style={itm}><b style={{color:COLORS.accentBright}}>{vv}</b> — {kk}</div>);
    return <div style={itm}>{String(v)}</div>;
  }
  return <div style={card}>
    {keys.map(k=><div key={k} style={{marginBottom:9}}><div style={lab}>{LBL[k]||k}</div>{renderVal(k,d[k])}</div>)}
  </div>;
}
// ---- Desktop split-view pieces (embedded only) ----
// (StageBar ยุบทิ้ง · stage+status รวมเป็น field เดียวแล้ว #33 → CTA state-driven แทน)
// link card (desktop) — icon + kind + label
const KIND_ICON={brief:"📋",raw:"🎬",edited:"✂️",final:"✅",reference:"🔖",repo:"💻",doc:"📄"};
function LinkCard({link, color, label, canEdit, onDel}){
  return <div style={{flex:"1 1 150px",minWidth:0,padding:"13px 14px",background:COLORS.card,borderRadius:12,
    border:`1px solid ${COLORS.hairline}`,display:"flex",flexDirection:"column",gap:8,position:"relative"}}>
    <div style={{display:"flex",alignItems:"center",gap:8}}>
      <span style={{width:30,height:30,borderRadius:8,background:COLORS.accentSoft,border:`1px solid ${COLORS.accentEdge}`,
        display:"flex",alignItems:"center",justifyContent:"center",flexShrink:0,fontSize:15}}>{KIND_ICON[link.kind]||"🔗"}</span>
      <div style={{fontSize:9.5,fontWeight:700,color:color,letterSpacing:1,fontFamily:MONO_FONT,textTransform:"uppercase",whiteSpace:"nowrap"}}>{link.kind}</div>
      {canEdit && <><div style={{flex:1}}/><button onClick={()=>onDel(link)} style={{background:"transparent",color:"var(--th-st-red-fg)",fontSize:16,lineHeight:1,cursor:"pointer"}}>×</button></>}
    </div>
    <a href={link.url} target="_blank" title={link.url} style={{fontSize:13,fontWeight:600,color:COLORS.inkBright,lineHeight:1.35,
      overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",textDecoration:"none"}}>{label}</a>
  </div>;
}
// audit timeline column (desktop)
function fmtAudit(a){
  let d={}; try{ d=JSON.parse(a.detail||"{}"); }catch(e){}
  if(a.action==="created") return "สร้างงาน";
  if(a.action==="add_link") return "เพิ่มลิงก์ "+(d.kind||"");
  if(a.action==="attach")   return "อัปไฟล์ "+(d.name||"");
  if(a.action==="review")   return d.decision==="approved"?"อนุมัติงาน":"ตีกลับงาน";
  if(a.action==="handoff")  return "ส่งต่อ → ขั้น "+(d.stage||"");
  if(a.action==="assigned") return "มอบหมายงาน";
  if(a.action==="update"){
    const keys=Object.keys(d);
    if(d.status) return "เปลี่ยนสถานะ → "+(statusTok(d.status.to).label||d.status.to);
    if(d.current_stage_id) return "ขยับ stage";
    if(d.owner_id) return "เปลี่ยนเจ้าของงาน";
    if(d.due) return "ปรับกำหนดส่ง";
    if(d.priority) return "ปรับ priority → "+d.priority.to;
    return "แก้ไข "+keys.join(", ");
  }
  return a.action;
}
function AuditTimeline({events}){
  const evs=events||[];
  if(!evs.length) return null;
  return <div>{evs.map((e,i)=>{ const last=i===evs.length-1;
    return <div key={e.id} style={{display:"flex",gap:11,position:"relative"}}>
      <div style={{flexShrink:0,width:14,display:"flex",flexDirection:"column",alignItems:"center",paddingTop:4}}>
        <span style={{width:last?11:8,height:last?11:8,borderRadius:999,
          background:last?COLORS.accentBright:COLORS.cardElev,border:`2px solid ${last?COLORS.accentBright:COLORS.hairlineStr}`,
          boxShadow:last?`0 0 12px ${COLORS.accentGlow}`:"none",zIndex:1}}/>
        {!last && <div style={{flex:1,width:1.5,background:COLORS.hairline,marginTop:2,marginBottom:-4}}/>}
      </div>
      <div style={{flex:1,paddingBottom:last?0:15,minWidth:0}}>
        <div style={{fontSize:13,fontWeight:last?700:600,color:last?COLORS.inkBright:COLORS.ink,lineHeight:1.35}}>{fmtAudit(e)}</div>
        <div style={{fontSize:10.5,color:COLORS.inkMuted,fontFamily:MONO_FONT,letterSpacing:0.4,marginTop:3}}>
          {e.imp_by_name ? <span style={{color:"var(--th-accent)"}}>{e.imp_by_name} 👁สวมบท {e.user_name||"?"}</span> : (e.user_name||"—")} · {(e.created_at||"").slice(5,16).replace("T"," ")}</div>
      </div>
    </div>; })}</div>;
}
// ---- รายละเอียดงาน + แทรกสื่อ Google Drive ----
// ดึง file id จากลิงก์ Google Drive ทุกแบบ (/file/d/ID, ?id=ID, /d/ID)
function driveId(url){ const m=(url||"").match(/\/file\/d\/([-\w]{20,})/)||(url||"").match(/[?&]id=([-\w]{20,})/)||(url||"").match(/\/d\/([-\w]{20,})/); return m?m[1]:null; }
// Google Docs/Sheets/Slides → preview URL ที่ถูก format (ไม่ใช่ /file/d/ ของ Drive ทั่วไป)
function gdocInfo(url){ const u=url||""; let m;
  if(m=u.match(/docs\.google\.com\/document\/d\/([-\w]{20,})/))     return {kind:"doc",  id:m[1],icon:"📄",label:"Google Docs",  preview:`https://docs.google.com/document/d/${m[1]}/preview`};
  if(m=u.match(/docs\.google\.com\/spreadsheets\/d\/([-\w]{20,})/)) return {kind:"sheet",id:m[1],icon:"📊",label:"Google Sheets",preview:`https://docs.google.com/spreadsheets/d/${m[1]}/preview`};
  if(m=u.match(/docs\.google\.com\/presentation\/d\/([-\w]{20,})/)) return {kind:"slide",id:m[1],icon:"📽",label:"Google Slides",preview:`https://docs.google.com/presentation/d/${m[1]}/preview`};
  return null; }
// จาก mime ของ Google native file → preview src (ใช้กับ attachment ที่เป็น Google Doc)
function gdocByMime(mime,fid){ const m=(mime||""); if(!fid) return null;
  if(m.includes("google-apps.document"))     return {icon:"📄",label:"Google Docs",  preview:`https://docs.google.com/document/d/${fid}/preview`};
  if(m.includes("google-apps.spreadsheet"))  return {icon:"📊",label:"Google Sheets",preview:`https://docs.google.com/spreadsheets/d/${fid}/preview`};
  if(m.includes("google-apps.presentation")) return {icon:"📽",label:"Google Slides",preview:`https://docs.google.com/presentation/d/${fid}/preview`};
  return null; }
function isHttp(s){ return /^https?:\/\/\S+$/i.test((s||"").trim()); }
// render รายละเอียด: บรรทัดที่เป็นลิงก์ Drive → ฝัง preview · ลิงก์รูป/วิดีโอตรง → img/video · ลิงก์อื่น → a · ที่เหลือ → ข้อความ
function MediaRenderer({text}){
  if(!text) return null;
  return <div>{(""+text).split(/\n/).map((ln,i)=>{
    const s=ln.trim(), gd=gdocInfo(s), did=gd?null:driveId(s);
    if(gd) return <div key={i} style={{margin:"8px 0",borderRadius:12,overflow:"hidden",border:`1px solid ${COLORS.hairline}`}}>
      <a href={s} target="_blank" rel="noopener" style={{display:"flex",alignItems:"center",gap:7,padding:"8px 11px",background:COLORS.card,fontSize:12.5,fontWeight:600,color:COLORS.inkBright,textDecoration:"none"}}><span>{gd.icon}</span><span>{gd.label}</span><span style={{marginLeft:"auto",color:COLORS.accentBright,fontSize:11.5}}>เปิด ↗</span></a>
      <iframe src={gd.preview} style={{width:"100%",height:420,border:"none",borderTop:`1px solid ${COLORS.hairline}`,background:"#fff",display:"block"}} loading="lazy"/></div>;
    if(did) return <div key={i} style={{margin:"8px 0",borderRadius:12,overflow:"hidden",border:`1px solid ${COLORS.hairline}`}}><iframe src={`https://drive.google.com/file/d/${did}/preview`} style={{width:"100%",height:360,border:"none",background:"#000",display:"block"}} allow="autoplay" allowFullScreen loading="lazy"/></div>;
    if(/^https?:\/\/\S+\.(jpg|jpeg|png|gif|webp|avif)(\?\S*)?$/i.test(s)) return <img key={i} src={s} loading="lazy" style={{maxWidth:"100%",borderRadius:12,margin:"8px 0",display:"block"}}/>;
    if(/^https?:\/\/\S+\.(mp4|webm|mov|m4v)(\?\S*)?$/i.test(s)) return <video key={i} src={s} controls style={{maxWidth:"100%",borderRadius:12,margin:"8px 0",display:"block",background:"#000"}}/>;
    if(isHttp(s)) return <a key={i} href={s} target="_blank" rel="noopener" style={{color:COLORS.accentBright,wordBreak:"break-all",fontSize:13.5,display:"block",margin:"3px 0"}}>{s}</a>;
    if(!s) return <div key={i} style={{height:7}}/>;
    return <div key={i} style={{fontSize:14,color:COLORS.ink,lineHeight:1.6,whiteSpace:"pre-wrap",wordBreak:"break-word"}}>{ln}</div>;
  })}</div>;
}
// ไฟล์แนบ (อัปขึ้น Drive แล้ว) → embed รูป/วิดีโอ · เอกสารเป็นลิงก์
function AttachItem({a, canEdit, onDel}){
  const m=(a.mime||"").toLowerCase(), fid=a.drive_file_id;
  const isImg=m.startsWith("image/"), isVid=m.startsWith("video/");
  const gd=gdocByMime(a.mime,fid);   // Google Docs/Sheets/Slides → embed แบบถูก format
  const sizeStr=a.size?(a.size>1048576?(a.size/1048576).toFixed(1)+" MB":Math.max(1,Math.round(a.size/1024))+" KB"):"";
  return <div style={{marginBottom:10,background:COLORS.card,border:`1px solid ${COLORS.hairline}`,borderRadius:12,overflow:"hidden"}}>
    <div style={{display:"flex",alignItems:"center",gap:8,padding:"9px 12px"}}>
      <span style={{fontSize:15}}>{gd?gd.icon:isImg?"🖼":isVid?"🎬":"📄"}</span>
      <a href={a.web_link||`https://drive.google.com/file/d/${fid}/view`} target="_blank" rel="noopener" style={{flex:1,minWidth:0,fontSize:13,fontWeight:600,color:COLORS.inkBright,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"}}>{a.name}</a>
      {gd&&<span style={{fontSize:10,color:COLORS.accentBright,fontWeight:700,whiteSpace:"nowrap"}}>แก้ไข ↗</span>}
      {sizeStr&&<span style={{fontSize:10.5,color:COLORS.inkSubtle,fontFamily:MONO_FONT,whiteSpace:"nowrap"}}>{sizeStr}</span>}
      {canEdit&&<button onClick={()=>onDel(a)} title="ลบไฟล์" style={{background:"transparent",color:"var(--th-st-red-fg)",fontSize:16,cursor:"pointer",lineHeight:1}}>×</button>}
    </div>
    {isImg&&fid&&<img src={`https://drive.google.com/thumbnail?id=${fid}&sz=w1600`} loading="lazy" style={{maxWidth:"100%",display:"block",borderTop:`1px solid ${COLORS.hairline}`}}/>}
    {isVid&&fid&&<iframe src={`https://drive.google.com/file/d/${fid}/preview`} style={{width:"100%",height:340,border:"none",borderTop:`1px solid ${COLORS.hairline}`,background:"#000",display:"block"}} allow="autoplay" allowFullScreen loading="lazy"/>}
    {gd&&<iframe src={gd.preview} style={{width:"100%",height:420,border:"none",borderTop:`1px solid ${COLORS.hairline}`,background:"#fff",display:"block"}} loading="lazy"/>}
  </div>;
}
// ---- Focus Mode: เริ่มงาน → จอมืดเต็มจอ + timer + checklist + done/เลื่อน ----
const FOCUS_CHEERS=["เยี่ยมไปเลย ✨","ลุยต่อ 🔥","โฟกัสสุดยอด 🎯","งานเดินแล้ว 💪","เก่งมาก!","ปังอีกชิ้น 🚀","มืออาชีพชัด ✨","สำเร็จแล้ว 🏆","ทำได้ดีมาก","อีกนิดเดียว สู้ๆ"];
function pickCheer(){ return FOCUS_CHEERS[Math.floor(Math.random()*FOCUS_CHEERS.length)]; }
function nextRoundHour(){ const d=new Date(); d.setTime(d.getTime()+3600000);   // now+1ชม. → ปัดขึ้นชั่วโมงเต็ม (12:45→14:00)
  if(d.getMinutes()>0||d.getSeconds()>0){ d.setHours(d.getHours()+1); } d.setMinutes(0,0,0); return d; }
function fmtDur(sec){ sec=Math.max(0,Math.floor(sec)); const h=Math.floor(sec/3600),m=Math.floor((sec%3600)/60),s=sec%60;
  const p=n=>String(n).padStart(2,"0"); return h>0?`${h}:${p(m)}:${p(s)}`:`${p(m)}:${p(s)}`; }
const FOCUS_BG={position:"fixed",inset:0,background:"var(--th-focus-bg)",zIndex:8000,display:"flex",alignItems:"center",justifyContent:"center",overflowY:"auto",padding:"32px 0"};
const FOCUS_INP={flex:1,padding:"11px 12px",borderRadius:10,background:"var(--th-focus-inp-bg)",border:"1px solid var(--th-focus-inp-border)",color:"var(--th-focus-ink)",fontSize:14,colorScheme:"var(--th-color-scheme)",minWidth:0};

function FocusOverlay({t, startedAt, clist, onToggle, onDone, onSnooze, onExit, done}){
  const [elapsed,setElapsed]=useState(0); const [snz,setSnz]=useState(false);
  const today=(()=>{ const d=new Date(); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`; })();
  const [snDate,setSnDate]=useState(today);
  const [snTime,setSnTime]=useState(()=>{ const d=nextRoundHour(); return `${String(d.getHours()).padStart(2,"0")}:${String(d.getMinutes()).padStart(2,"0")}`; });
  useEffect(()=>{ if(done) return; const base=new Date(startedAt).getTime();
    const tick=()=>setElapsed((Date.now()-base)/1000); tick(); const iv=setInterval(tick,1000); return ()=>clearInterval(iv); },[startedAt,done]);
  const total=clist.length, dn=clist.filter(x=>x.done).length, pct=total?Math.round(dn/total*100):0;
  const inner = done ? (
    <div style={FOCUS_BG}><div style={{textAlign:"center",maxWidth:440,padding:24}}>
      <div style={{fontSize:64,marginBottom:6}}>🔥</div>
      <div style={{fontSize:27,fontWeight:800,color:"var(--th-focus-ink)",marginBottom:6}}>โฟกัส {done.minutes} นาที</div>
      <div style={{fontSize:18,color:COLORS.accentBright,fontWeight:700,marginBottom:30}}>{done.cheer}</div>
      <button onClick={onExit} style={{padding:"13px 34px",borderRadius:14,background:COLORS.accent,color:"var(--th-on-accent)",fontWeight:700,fontSize:15,cursor:"pointer"}}>เยี่ยม! กลับสู่งาน</button>
    </div></div>) : (
    <div style={FOCUS_BG}>
      <button onClick={onExit} title="ออกจากโฟกัส (ไม่ปิดงาน)" style={{position:"absolute",top:18,right:22,background:"transparent",color:"var(--th-focus-ink-dim)",fontSize:26,cursor:"pointer",lineHeight:1}}>✕</button>
      <div style={{width:"100%",maxWidth:560,padding:"0 24px"}}>
        <div style={{fontSize:12,letterSpacing:2,color:"var(--th-focus-ink-faint)",fontFamily:MONO_FONT,textTransform:"uppercase",marginBottom:12}}>กำลังโฟกัส</div>
        <div style={{fontSize:29,fontWeight:800,color:"var(--th-focus-ink)",lineHeight:1.25,marginBottom:14}}>{t.title}</div>
        <div style={{fontSize:52,fontWeight:800,fontFamily:NUM_FONT,color:COLORS.accentBright,letterSpacing:2,marginBottom:20}}>{fmtDur(elapsed)}</div>
        {total>0 && <><div style={{display:"flex",justifyContent:"space-between",fontSize:13,color:"var(--th-focus-ink-mid)",marginBottom:6}}><span>ความคืบหน้า</span><span style={{fontFamily:NUM_FONT,fontWeight:700}}>{dn}/{total} · {pct}%</span></div>
          <div style={{height:8,borderRadius:999,background:"var(--th-focus-track)",overflow:"hidden",marginBottom:20}}><div style={{height:"100%",width:pct+"%",background:STATUS.done.dot,borderRadius:999,transition:"width .3s"}}/></div></>}
        {t.description && <div style={{fontSize:14,color:"var(--th-focus-ink-soft)",lineHeight:1.5,marginBottom:18,maxHeight:120,overflowY:"auto",whiteSpace:"pre-wrap"}}>{t.description}</div>}
        <div style={{marginBottom:22,maxHeight:240,overflowY:"auto"}}>
          {clist.map((it,i)=><div key={i} onClick={()=>onToggle(i)} style={{display:"flex",alignItems:"center",gap:11,padding:"9px 0",cursor:"pointer"}}>
            <span style={{flexShrink:0,width:22,height:22,borderRadius:6,border:`2px solid ${it.done?STATUS.done.dot:"var(--th-focus-chk-border)"}`,background:it.done?STATUS.done.dot:"transparent",color:"var(--th-focus-chk-ink)",fontSize:13,fontWeight:900,display:"flex",alignItems:"center",justifyContent:"center"}}>{it.done?"✓":""}</span>
            <span style={{flex:1,fontSize:16,color:it.done?"var(--th-focus-ink-faint)":"var(--th-focus-ink)",textDecoration:it.done?"line-through":"none",lineHeight:1.4}}>{it.text}</span>
          </div>)}
          {total===0 && <div style={{fontSize:13,color:"var(--th-focus-ink-ghost)",fontStyle:"italic"}}>ยังไม่มีเช็กลิสต์ · เพิ่มได้จากหน้ารายละเอียด (เร็วๆนี้ ✨ AI ช่วยสร้าง)</div>}
        </div>
        {snz ? <div style={{background:"var(--th-focus-card)",borderRadius:16,padding:16}}>
          <div style={{fontSize:14,color:"var(--th-focus-ink)",fontWeight:700,marginBottom:10}}>เลื่อนไปทำเมื่อไหร่?</div>
          <div style={{display:"flex",gap:8,marginBottom:12}}><input type="date" value={snDate} onChange={e=>setSnDate(e.target.value)} style={FOCUS_INP}/><input type="time" value={snTime} onChange={e=>setSnTime(e.target.value)} style={FOCUS_INP}/></div>
          <div style={{display:"flex",gap:8}}>
            <button onClick={()=>onSnooze(snDate, snDate+"T"+snTime)} style={{flex:1,padding:12,borderRadius:12,background:COLORS.accent,color:"var(--th-on-accent)",fontWeight:700,cursor:"pointer"}}>ยืนยันเลื่อน</button>
            <button onClick={()=>setSnz(false)} style={{padding:"12px 18px",borderRadius:12,background:"transparent",color:"var(--th-focus-btn-ink-2)",border:"1px solid var(--th-focus-btn-border-2)",cursor:"pointer"}}>กลับ</button>
          </div>
        </div> : <div style={{display:"flex",gap:10}}>
          <button onClick={onDone} style={{flex:1,padding:15,borderRadius:14,background:STATUS.done.dot,color:"var(--th-focus-chk-ink)",fontWeight:800,fontSize:16,cursor:"pointer"}}>✓ เสร็จแล้ว</button>
          <button onClick={()=>setSnz(true)} style={{padding:"15px 24px",borderRadius:14,background:"transparent",color:"var(--th-focus-btn-ink)",border:"1px solid var(--th-focus-btn-border)",fontWeight:700,fontSize:15,cursor:"pointer"}}>⏰ เลื่อน</button>
        </div>}
      </div>
    </div>);
  // portal ไป document.body เพื่อให้ overlay หลุดจาก stacking context ของ modal → คลุมเต็มจอจริง (รวม header)
  return ReactDOM.createPortal(inner, document.body);
}

function Detail({task, user, onClose, onChanged, canEdit, embedded, users, canSeeAll}){
  const [t,setT]=useState(task); const [busy,setBusy]=useState(false);
  const [reviews,setReviews]=useState([]); const [note,setNote]=useState("");
  const [links,setLinks]=useState([]); const [edit,setEdit]=useState(false); const [ef,setEf]=useState({});
  const [nl,setNl]=useState({kind:"brief",url:""});
  const [comments,setComments]=useState([]); const [cbody,setCbody]=useState("");
  const [addingLink,setAddingLink]=useState(false); const [cFocus,setCFocus]=useState(false); const [audit,setAudit]=useState([]);
  const [docTitles,setDocTitles]=useState({});   // kb/#id → ชื่อ doc จริง (label)
  const [clist,setClist]=useState(()=>{try{return JSON.parse(task.checklist||"[]");}catch(e){return [];}});
  const [newItem,setNewItem]=useState(""); const [mediaUrl,setMediaUrl]=useState("");
  const [attachments,setAttachments]=useState([]); const [uploading,setUploading]=useState(false); const [upPct,setUpPct]=useState(0); const [dragOver,setDragOver]=useState(false);
  const [focusOn,setFocusOn]=useState(false); const [focusStart,setFocusStart]=useState(null); const [focusDone,setFocusDone]=useState(null);
  useEffect(()=>{ try{setClist(JSON.parse(t.checklist||"[]"));}catch(e){setClist([]);} },[t.id,t.checklist]);
  useEffect(()=>{ setT(task); },[task.id, task.updated_at]);   // resync เมื่อข้อมูลงานเปลี่ยน (poll/แก้จากที่อื่น)
  useEscClose(embedded?null:onClose);   // modal (mobile/kanban) ปิดด้วย Esc · embedded ใช้ปุ่มล้าง
  function loadComments(){ api("GET","/api/tasks/"+t.id+"/comments").then(setComments).catch(()=>{}); }
  function loadAttachments(){ api("GET","/api/tasks/"+t.id+"/attachments").then(setAttachments).catch(()=>{}); }
  function uploadFiles(files){ const list=[...(files||[])].filter(Boolean); if(!list.length)return; let i=0;
    const next=()=>{ if(i>=list.length){ setUploading(false); setUpPct(0); loadAttachments(); onChanged&&onChanged(); return; }
      const f=list[i++], fd=new FormData(); fd.append("file",f);
      const xhr=new XMLHttpRequest(); xhr.open("POST",API+"/api/tasks/"+t.id+"/attachments");
      if(token()) xhr.setRequestHeader("Authorization","Bearer "+token()); xhr.withCredentials=true;
      xhr.upload.onprogress=e=>{ if(e.lengthComputable) setUpPct(Math.round(e.loaded/e.total*100)); };
      xhr.onload=()=>{ if(xhr.status>=200&&xhr.status<300){ toast.ok("อัปไฟล์แล้ว · "+f.name); } else { let m="อัปไฟล์ไม่สำเร็จ"; try{m=JSON.parse(xhr.responseText).detail||m;}catch(_){} toast.error(m); } setUpPct(0); next(); };
      xhr.onerror=()=>{ toast.error("เครือข่ายขัดข้องระหว่างอัปไฟล์"); setUpPct(0); next(); };
      xhr.send(fd); };
    setUploading(true); next(); }
  async function delAttachment(a){ if(!a)return;
    if(!await gate({title:"ลบไฟล์นี้?",body:`"${a.name||"ไฟล์"}" จะถูกลบออกจาก Google Drive · กู้คืนไม่ได้`,danger:true})) return;
    api("DELETE","/api/attachments/"+a.id).then(()=>{ loadAttachments();
    toast("ลบไฟล์แล้ว",{actionLabel:"",}); }).catch(e=>toast.error(e.message)); }
  async function addComment(){ const b=cbody.trim(); if(!b)return; setBusy(true);
    try{ await api("POST","/api/tasks/"+t.id+"/comments",{body:b}); setCbody(""); loadComments(); }
    catch(e){toast.error(e.message);} finally{setBusy(false);} }
  function linkText(l){ const m=(l.url||"").match(/\/kb\/#(\d+)$/); return (m&&docTitles[m[1]]) ? ("📄 "+docTitles[m[1]]) : l.url; }
  const isReviewer=["head_content","admin","superadmin"].includes(user.role);
  function loadLinks(){ api("GET","/api/tasks/"+t.id+"/links").then(ls=>{ setLinks(ls);
    ls.forEach(l=>{ const m=(l.url||"").match(/\/kb\/#(\d+)$/); if(m){ api("GET","/api/docs/"+m[1]).then(d=>setDocTitles(prev=>prev[m[1]]?prev:{...prev,[m[1]]:d.title})).catch(()=>{}); } });
  }).catch(()=>{}); }
  useEffect(()=>{ api("GET","/api/tasks/"+t.id+"/reviews").then(setReviews).catch(()=>{}); loadLinks(); loadComments(); loadAttachments();
    api("GET","/api/tasks/"+t.id+"/audit").then(a=>setAudit(a||[])).catch(()=>{}); },[t.id]);
  async function patch(body){ setBusy(true); try{ const r=await api("PATCH","/api/tasks/"+t.id,body); setT(r.task); onChanged&&onChanged(); }catch(e){toast.error(e.message);} finally{setBusy(false);} }
  function startEdit(){ setEf({title:t.title,due:t.due||"",brand:t.brand||"",priority:t.priority,owner_id:t.owner_id||"",recurrence:t.recurrence||"",tags:t.tags||"",description:t.description||""}); setEdit(true); }
  async function saveEdit(){
    if(!await gate({title:"บันทึกการแก้ไข?",body:`บันทึกการเปลี่ยนแปลงของ "${t.title}"`})) return;   // ถามเฉพาะตอนสวมบท
    const body={...ef}; if("owner_id" in body) body.owner_id=body.owner_id?+body.owner_id:null; await patch(body); setEdit(false); }
  async function addLink(){ if(!nl.url)return; setBusy(true); try{ await api("POST","/api/tasks/"+t.id+"/links",nl); setNl({kind:"brief",url:""}); setAddingLink(false); loadLinks(); }catch(e){toast.error(e.message);} finally{setBusy(false);} }
  async function delLink(link){ setBusy(true); try{ await api("DELETE","/api/links/"+link.id);
    toast("ลบลิงก์แล้ว",{actionLabel:"เลิกทำ",action:()=>{ api("POST","/api/tasks/"+t.id+"/links",{kind:link.kind,url:link.url}).then(loadLinks).catch(()=>{}); }});
    loadLinks(); }catch(e){toast.error(e.message);} finally{setBusy(false);} }
  async function review(decision){
    const ret=decision==="returned";
    if(!await gate({title:ret?"ตีกลับงาน?":"อนุมัติงาน?",body:ret?`ตีกลับ "${t.title}" ให้แก้ไข + แจ้งเจ้าของงาน`:`อนุมัติ "${t.title}" · แจ้งเจ้าของงาน`,always:true,danger:ret,confirmLabel:ret?"ยืนยันตีกลับ":"ยืนยันอนุมัติ"})) return;
    setBusy(true); try{ const r=await api("POST","/api/tasks/"+t.id+"/review",{decision,notes:note||null}); setT(r.task); setNote(""); api("GET","/api/tasks/"+t.id+"/reviews").then(setReviews); onChanged&&onChanged(); }catch(e){toast.error(e.message);} finally{setBusy(false);} }
  function rescheduleDays(n){ const d=new Date(); d.setHours(0,0,0,0); d.setDate(d.getDate()+n);
    const ds=`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`;
    patch({due:ds}); toast.ok("เลื่อนกำหนด → "+ds); }
  function saveClist(next){ setClist(next); patch({checklist:next}); }
  async function createGDoc(kind){ if(busy)return; setBusy(true);
    try{ const a=await api("POST","/api/tasks/"+t.id+"/gdoc",{kind});
      toast.ok((kind==="sheet"?"สร้าง Google Sheets":kind==="slide"?"สร้าง Google Slides":"สร้าง Google Docs")+" แล้ว · เปิดแท็บใหม่ให้แก้ไข");
      if(a.web_link){ try{ window.open(a.web_link,"_blank","noopener"); }catch(_){} }
      loadAttachments(); onChanged&&onChanged();
    }catch(e){ toast.error(e.message); } finally{ setBusy(false); } }
  async function startFocus(){ try{ const r=await api("POST","/api/tasks/"+t.id+"/focus/start"); setFocusStart(r.started_at); setFocusDone(null); setFocusOn(true); onChanged&&onChanged(); }catch(e){toast.error(e.message);} }
  async function focusComplete(){ try{ const r=await api("POST","/api/tasks/"+t.id+"/focus/done"); setFocusDone({minutes:r.minutes,cheer:pickCheer()}); api("GET","/api/tasks/"+t.id).then(setT).catch(()=>{}); onChanged&&onChanged(); }catch(e){toast.error(e.message);} }
  async function focusSnooze(due,snooze_to){ try{ await api("POST","/api/tasks/"+t.id+"/focus/snooze",{due,snooze_to}); toast.ok("เลื่อนไป "+(snooze_to||due).replace("T"," ")); setFocusOn(false); api("GET","/api/tasks/"+t.id).then(setT).catch(()=>{}); onChanged&&onChanged(); }catch(e){toast.error(e.message);} }
  async function cancelTask(){ if(t.status==="cancelled")return;
    if(!await gate({title:"ยกเลิกงาน?",body:`"${t.title}" จะถูกยกเลิก · ยังเก็บในคลังงาน (เปิดใหม่ได้)`,danger:true,confirmLabel:"ยืนยันยกเลิก"})) return;
    await patch({status:"cancelled"}); toast.ok("ยกเลิกงานแล้ว · เก็บในคลังงาน"); }
  async function deleteTask(){
    if(!await gate({title:"ลบงานนี้?",body:`"${t.title}" จะย้ายลงถังขยะ · กู้คืนได้ 30 วัน`,danger:true})) return;
    setBusy(true); try{ await api("DELETE","/api/tasks/"+t.id); toast.ok("ย้ายลงถังขยะแล้ว · กู้คืนได้ 30 วัน"); onChanged&&onChanged(); onClose&&onClose(); }catch(e){ toast.error(e.message); } finally{ setBusy(false); } }
  const canDelete=["admin","service","superadmin"].includes(user.role);
  function insertMedia(){ const u=mediaUrl.trim(); if(!u)return; if(!isHttp(u)){ toast.error("ต้องเป็นลิงก์ http(s)"); return; }
    setEf(s=>({...s,description:((s.description||"").trim()+"\n"+u).trim()})); setMediaUrl(""); toast.ok(driveId(u)?"แทรกสื่อ Drive แล้ว":"แทรกลิงก์แล้ว"); }
  // ── redesign: overflow / doc-menu / accordion / owner-pick UI state ──
  const [ovfOpen,setOvfOpen]=useState(false); const [docOpen,setDocOpen]=useState(false);
  // viewRole: map ระบบ → worker / leader / ceo (disclosure)
  const viewRole = ["superadmin","exec","service"].includes(user.role) ? "ceo"
                 : ["head_content","admin","pm","head_review"].includes(user.role) ? "leader" : "worker";
  const isLeader = viewRole!=="worker";
  const [accOpen,setAccOpen]=useState(isLeader);   // leader/ceo เปิดประวัติอัตโนมัติ · worker ปิด
  const [pickOwner,setPickOwner]=useState(false);  // เปิด select มอบหมาย (leader)
  // ── state-driven CTA: map status จริง (หลายค่า) → 5 state ──
  const CTA_STATE = (s=>{
    if(["idea","not_started","pending","received","brief_received"].includes(s)) return "idea";
    if(s==="blocked") return "blocked";
    if(["submitted","review","to_publish"].includes(s)) return "review";
    if(["approved","done","deployed","published"].includes(s)) return "done";
    if(s==="cancelled") return "cancelled";
    // progress / in_progress / editing / filming / shoot / testing / revision / returned → ลงมือ
    return "progress";
  })(t.status);
  const ctaState=CTA_STATE;
  const terminalSt=["done","cancelled","approved","published","deployed"].includes(t.status);
  // CTA actions (เปลี่ยน status ผ่าน patch เดิม + log อัตโนมัติฝั่ง backend)
  async function ctaStart(){ if(await gate({title:"เริ่มทำงานนี้?",body:`"${t.title}" → กำลังทำ`})) patch({status:"progress"}); }
  async function ctaComplete(){ if(await gate({title:"ส่งงานนี้?",body:`"${t.title}" → ส่งให้รีวิว`,always:true,confirmLabel:"ยืนยันส่งงาน"})) patch({status:"review"}); }
  async function ctaResume(){ patch({status:"progress"}); }
  const p=pipeTok(t.pipeline_key);
  const di=dueInfo(t.due,t.status);
  const ownerName=t.owner_name||"—";
  const usersArr=users||[];
  // ── เลเบล mono เล็ก (subhead/zlabel) ──
  const ZLBL={fontSize:11,color:COLORS.inkSubtle,fontFamily:MONO_FONT,fontWeight:700,letterSpacing:1.3,textTransform:"uppercase",display:"flex",alignItems:"center",gap:9,marginBottom:14};
  const ZCNT={fontFamily:NUM_FONT,fontSize:11,fontWeight:700,color:COLORS.inkMuted,background:"var(--th-row-soft-2)",padding:"1px 9px",borderRadius:999,letterSpacing:0};
  const SUBHEAD={fontSize:11.5,color:COLORS.inkSubtle,fontFamily:MONO_FONT,fontWeight:700,letterSpacing:1.1,textTransform:"uppercase",marginBottom:12,display:"flex",alignItems:"center",gap:8};

  // ── โซน "ลงมือ + ส่งงาน" (ซ้าย) ──
  const ctaBtnBase={width:"100%",padding:16,borderRadius:15,border:"none",fontWeight:800,fontSize:16,display:"flex",alignItems:"center",justifyContent:"center",gap:9,letterSpacing:0.2,cursor:"pointer"};
  const ctaBlock=(()=>{
    if(!canEdit) return null;
    if(ctaState==="idea") return <button disabled={busy} onClick={ctaStart} style={{...ctaBtnBase,background:`linear-gradient(135deg,${COLORS.accentBright},${COLORS.accent})`,color:"var(--th-on-accent)",boxShadow:`0 8px 26px ${COLORS.accentGlow}`}}>▶ เริ่มทำงานนี้</button>;
    if(ctaState==="progress") return <button disabled={busy} onClick={ctaComplete} style={{...ctaBtnBase,background:`linear-gradient(135deg,${STATUS.done.dot},color-mix(in srgb,${STATUS.done.dot} 78%,#000))`,color:"var(--th-green-ink)",boxShadow:`0 8px 26px ${STATUS.done.bg}`}}>✓ ทำเสร็จแล้ว</button>;
    if(ctaState==="blocked") return <button disabled={busy} onClick={ctaResume} style={{...ctaBtnBase,background:"linear-gradient(135deg,var(--th-st-orange-dot),var(--th-st-orange-fg))",color:"var(--th-green-ink)",boxShadow:"0 8px 24px var(--th-st-orange-bg)"}}>↩ กลับมาทำต่อ</button>;
    if(ctaState==="review") return <button disabled style={{...ctaBtnBase,background:"var(--th-row-soft-2)",color:COLORS.inkMuted,cursor:"default",border:`1px solid ${COLORS.hairline}`,boxShadow:"none"}}>⏳ กำลังรอรีวิว</button>;
    if(ctaState==="cancelled") return <button disabled={busy} onClick={()=>patch({status:"progress"})} style={{...ctaBtnBase,background:STATUS.progress.bg,color:STATUS.progress.fg,border:`1px solid ${STATUS.progress.dot}`,boxShadow:"none"}}>↩ เปิดงานใหม่</button>;
    return <div style={{...ctaBtnBase,background:STATUS.done.bg,color:STATUS.done.fg,border:`1px solid ${STATUS.done.dot}`,cursor:"default"}}>✓ งานนี้เสร็จแล้ว</div>;
  })();
  // blocked banner (เฉพาะ state=blocked)
  const blockBanner = ctaState==="blocked" && <div style={{display:"flex",alignItems:"flex-start",gap:11,marginTop:14,padding:"12px 14px",borderRadius:13,background:STATUS.blocked.bg,border:`1px solid color-mix(in srgb, ${STATUS.blocked.dot} 45%, transparent)`}}>
    <span style={{fontSize:16,lineHeight:1.2}}>🚩</span>
    <span style={{fontSize:12.5,color:COLORS.ink,lineHeight:1.5}}><b style={{color:STATUS.blocked.fg,fontWeight:700}}>ติดปัญหา:</b> งานนี้ถูกแจ้งว่าติดปัญหา — เคลียร์แล้วกด "กลับมาทำต่อ"</span>
  </div>;
  // sec-row: Focus + แจ้งปัญหา
  const ghost={padding:"9px 15px",borderRadius:999,background:"transparent",cursor:"pointer",fontSize:12.5,fontWeight:600,display:"inline-flex",alignItems:"center",gap:6};
  const secRow = canEdit && !terminalSt && <div style={{display:"flex",gap:9,flexWrap:"wrap",marginTop:12}}>
    <button disabled={busy} onClick={startFocus} style={{...ghost,color:COLORS.accentBright,border:`1px solid ${COLORS.accentEdge}`}}>▷ เปิด Focus Mode</button>
    {ctaState!=="blocked" && <button disabled={busy} onClick={()=>patch({status:"blocked"})} style={{...ghost,color:STATUS.blocked.fg,border:`1px solid color-mix(in srgb, ${STATUS.blocked.dot} 45%, transparent)`}}>🚩 แจ้งติดปัญหา</button>}
  </div>;
  // ส่งงาน: dropzone + file list + +สร้างเอกสาร▾ + +เพิ่มลิงก์
  const submitZone = (canEdit||attachments.length>0||links.length>0) && <>
    <div style={{height:1,background:COLORS.hairline,margin:"18px 0 16px"}}/>
    <div style={SUBHEAD}>📤 ส่งงาน</div>
    {canEdit && <label
        onDragOver={e=>{e.preventDefault();setDragOver(true);}}
        onDragLeave={()=>setDragOver(false)}
        onDrop={e=>{e.preventDefault();setDragOver(false);uploadFiles(e.dataTransfer.files);}}
        style={{display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",gap:7,padding:"24px 18px",borderRadius:14,border:`1.5px dashed ${dragOver?COLORS.accentBright:COLORS.hairlineStr}`,cursor:uploading?"wait":"pointer",textAlign:"center",marginBottom:12,background:dragOver?COLORS.accentSoft:"var(--th-row-soft)"}}>
      <input type="file" multiple onChange={e=>{uploadFiles(e.target.files); e.target.value="";}} disabled={uploading} style={{display:"none"}}/>
      {uploading ? <><div style={{fontSize:13.5,color:COLORS.accentBright,fontWeight:600}}>กำลังอัป… {upPct}%</div>
        <div style={{width:"70%",height:4,borderRadius:999,background:"var(--th-track-2)",overflow:"hidden"}}><div style={{height:"100%",width:upPct+"%",background:COLORS.accent,transition:"width .2s"}}/></div></>
      : <><span style={{fontSize:25}}>📎</span><span style={{fontSize:13.5,color:COLORS.ink,fontWeight:600}}>ทำงานเสร็จแล้ว — ลากไฟล์มาวางที่นี่เพื่อส่ง</span><span style={{fontSize:11.5,color:COLORS.inkSubtle}}>หรือคลิกเพื่อเลือกไฟล์ · ขึ้น Google Drive ให้อัตโนมัติ</span></>}
    </label>}
    {attachments.map(a=><AttachItem key={a.id} a={a} canEdit={canEdit} onDel={delAttachment}/>)}
    {links.length>0 && <div style={{display:"flex",gap:10,flexWrap:"wrap",margin:"4px 0 8px"}}>{links.map(l=><LinkCard key={l.id} link={l} color={p.color} label={linkText(l)} canEdit={canEdit} onDel={delLink}/>)}</div>}
    {canEdit && <div style={{display:"flex",gap:9,flexWrap:"wrap",marginTop:6,position:"relative"}}>
      <button disabled={busy} onClick={e=>{e.stopPropagation();createGDoc("doc");}} style={{padding:"8px 14px",borderRadius:999,background:COLORS.accentSoft,color:COLORS.accentBright,border:`1px solid ${COLORS.accentEdge}`,fontSize:12.5,fontWeight:600,cursor:busy?"wait":"pointer",display:"inline-flex",alignItems:"center",gap:6}}>+ สร้าง Google Doc</button>
      {!addingLink && <button onClick={()=>setAddingLink(true)} style={{padding:"8px 13px",borderRadius:999,background:"transparent",color:COLORS.inkMuted,border:`1px dashed ${COLORS.hairlineStr}`,fontSize:12.5,fontWeight:600,cursor:"pointer"}}>+ เพิ่มลิงก์</button>}
      {/* doc dropdown ถูกเอาออก — สร้างได้แค่ Google Doc (คุณเอก 23 มิ.ย.) */}
    </div>}
    {canEdit && addingLink && <div style={{display:"flex",gap:6,marginTop:8,flexWrap:"wrap"}}>
      <select value={nl.kind} onChange={e=>setNl({...nl,kind:e.target.value})} style={{padding:"9px 8px",borderRadius:10,background:COLORS.card,border:`1px solid ${COLORS.hairline}`,color:COLORS.ink,fontSize:13}}>{["brief","raw","edited","final","reference","repo","doc"].map(k=><option key={k} value={k}>{k}</option>)}</select>
      <input autoFocus value={nl.url} onChange={e=>setNl({...nl,url:e.target.value})} onKeyDown={e=>{if(e.key==="Enter")addLink();}} placeholder="วาง URL" style={{flex:1,minWidth:120,padding:"9px 10px",borderRadius:10,background:COLORS.card,border:`1px solid ${COLORS.hairline}`,color:COLORS.ink,fontSize:13}}/>
      <button disabled={busy} onClick={addLink} style={{padding:"9px 14px",borderRadius:10,background:COLORS.accent,color:"var(--th-on-accent)",fontWeight:700,border:"none",cursor:"pointer"}}>เพิ่ม</button>
      <button onClick={()=>setAddingLink(false)} style={{padding:"9px 11px",borderRadius:10,background:"transparent",color:COLORS.inkMuted,border:`1px solid ${COLORS.hairline}`,cursor:"pointer"}}>✕</button>
    </div>}
  </>;
  const zoneAction = <div style={{background:COLORS.card,border:`1px solid ${COLORS.hairline}`,borderRadius:18,padding:"18px 19px"}}>
    <div style={ZLBL}>ลงมือ</div>
    {ctaBlock}
    {blockBanner}
    {secRow}
    {submitZone}
  </div>;

  // ── โซน "เช็กลิสต์" (ขวา) ──
  const clDone=clist.filter(x=>x.done).length, clPct=clist.length?Math.round(clDone/clist.length*100):0;
  const zoneChecklist = (canEdit||clist.length>0) && <div style={{background:COLORS.cardSoft,border:`1px solid ${COLORS.hairline}`,borderRadius:18,padding:"18px 19px"}}>
    <div style={ZLBL}>เช็กลิสต์ {clist.length>0 && <span style={ZCNT}>{clDone}/{clist.length}</span>}</div>
    {clist.length>0 && <div style={{height:6,borderRadius:999,background:"var(--th-track)",marginBottom:15,overflow:"hidden"}}><div style={{height:"100%",width:clPct+"%",background:`linear-gradient(90deg,${STATUS.done.dot},${STATUS.done.fg})`,borderRadius:999,transition:"width .3s"}}/></div>}
    {clist.map((it,i)=><div key={i} style={{display:"flex",alignItems:"center",gap:12,padding:"6px 0"}}>
      <button disabled={!canEdit} onClick={()=>saveClist(clist.map((x,j)=>j===i?{...x,done:!x.done}:x))} style={{flexShrink:0,width:20,height:20,borderRadius:7,cursor:canEdit?"pointer":"default",border:`2px solid ${it.done?STATUS.done.dot:COLORS.hairlineStr}`,background:it.done?STATUS.done.dot:"transparent",color:"var(--th-green-ink)",fontSize:11,fontWeight:900,display:"flex",alignItems:"center",justifyContent:"center"}}>{it.done?"✓":""}</button>
      <span style={{flex:1,fontSize:14,color:it.done?COLORS.inkSubtle:COLORS.ink,textDecoration:it.done?"line-through":"none",lineHeight:1.45,wordBreak:"break-word"}}>{it.text}</span>
      {canEdit && <button onClick={()=>saveClist(clist.filter((_,j)=>j!==i))} style={{background:"transparent",color:COLORS.inkSubtle,fontSize:15,cursor:"pointer",lineHeight:1}}>×</button>}
    </div>)}
    {canEdit && <input value={newItem} onChange={e=>setNewItem(e.target.value)} onKeyDown={e=>{ if(e.key==="Enter"){ const v=newItem.trim(); if(v){ saveClist([...clist,{text:v,done:false}]); setNewItem(""); } } }} placeholder="+ เพิ่มขั้นตอนย่อย (Enter)" style={{width:"100%",marginTop:11,padding:"10px 13px",borderRadius:11,background:"var(--th-row-soft)",border:`1px solid ${COLORS.hairline}`,color:COLORS.ink,fontSize:13.5,outline:"none"}}/>}
  </div>;

  // ── review card (leader/ceo + state=review) ──
  const zoneReview = isLeader && isReviewer && ctaState==="review" && <div style={{background:COLORS.card,border:`1px solid color-mix(in srgb, var(--th-st-orange-fg) 40%, transparent)`,borderRadius:18,padding:"18px 19px"}}>
    <div style={{display:"flex",alignItems:"center",gap:9,marginBottom:6}}>
      <span style={{width:8,height:8,borderRadius:999,background:"var(--th-st-orange-fg)",boxShadow:"0 0 8px var(--th-st-orange-fg)"}}/>
      <b style={{fontSize:13.5,color:COLORS.inkBright,fontWeight:700}}>รอคุณรีวิว</b>
    </div>
    <p style={{fontSize:12.5,color:COLORS.inkMuted,lineHeight:1.5,marginBottom:14}}>{t.owner_name||"เจ้าของงาน"}ส่งงานเข้ามาแล้ว{attachments.length?` · มี ${attachments.length} ไฟล์แนบ`:""}{clist.length?` · เช็กลิสต์ ${clDone}/${clist.length}`:""} — ตรวจแล้วกดอนุมัติหรือตีกลับพร้อมโน้ต</p>
    <textarea value={note} onChange={e=>setNote(e.target.value)} placeholder="โน้ตถึงเจ้าของงาน (ไม่บังคับ)" rows={2} style={{width:"100%",padding:"10px 12px",borderRadius:11,background:COLORS.card,border:`1px solid ${COLORS.hairline}`,color:COLORS.ink,fontSize:13.5,marginBottom:12,resize:"vertical",fontFamily:"inherit"}}/>
    <div style={{display:"flex",gap:10}}>
      <button disabled={busy} onClick={()=>review("approved")} style={{flex:1,padding:12,borderRadius:12,border:"none",cursor:"pointer",fontWeight:700,fontSize:13.5,background:`linear-gradient(135deg,${STATUS.done.dot},color-mix(in srgb,${STATUS.done.dot} 78%,#000))`,color:"var(--th-green-ink)",boxShadow:`0 8px 24px ${STATUS.done.bg}`}}>✓ อนุมัติงาน</button>
      <button disabled={busy} onClick={()=>review("returned")} style={{padding:"12px 18px",borderRadius:12,cursor:"pointer",fontWeight:700,fontSize:13.5,background:"transparent",color:"var(--th-st-orange-fg)",border:`1px solid color-mix(in srgb, var(--th-st-orange-fg) 45%, transparent)`}}>↩ ตีกลับแก้</button>
    </div>
  </div>;

  // ── accordion พูดคุย & ประวัติ ──
  const discCount=comments.length+audit.length+reviews.length;
  const zoneDiscuss = <div style={{border:`1px solid ${COLORS.hairline}`,borderRadius:18,overflow:"hidden",background:COLORS.cardSoft}}>
    <button onClick={()=>setAccOpen(o=>!o)} style={{listStyle:"none",cursor:"pointer",width:"100%",padding:"15px 18px",display:"flex",alignItems:"center",gap:10,fontSize:12,fontWeight:700,color:COLORS.inkMuted,fontFamily:MONO_FONT,letterSpacing:1,textTransform:"uppercase",background:"transparent",border:"none",textAlign:"left"}}>
      พูดคุย &amp; ประวัติ {discCount>0 && <span style={ZCNT}>{discCount}</span>}
      <span style={{marginLeft:"auto",transition:".22s",color:COLORS.inkSubtle,fontSize:11,transform:accOpen?"rotate(180deg)":"none",display:"inline-block"}}>▾</span>
    </button>
    {accOpen && <div style={{padding:"4px 18px 18px"}}>
      {comments.map(cm=><div key={cm.id} style={{background:COLORS.card,border:`1px solid ${COLORS.hairline}`,borderRadius:13,padding:"12px 14px",marginBottom:9}}>
        <div style={{display:"flex",alignItems:"center",gap:9,marginBottom:6}}>
          <span style={{width:23,height:23,borderRadius:999,background:COLORS.accentSoft,border:`1px solid ${COLORS.accentEdge}`,display:"flex",alignItems:"center",justifyContent:"center",fontSize:10.5,fontWeight:700,color:COLORS.accentBright,flexShrink:0}}>{(cm.user_name||"?").slice(0,1)}</span>
          <span style={{fontSize:13,fontWeight:700,color:COLORS.inkBright}}>{cm.user_name||"—"}</span>
          <span style={{fontSize:10,color:COLORS.inkSubtle,fontFamily:MONO_FONT,marginLeft:"auto"}}>{(cm.created_at||"").slice(5,16).replace("T"," ")}</span>
        </div>
        <div style={{fontSize:13.5,color:COLORS.ink,lineHeight:1.55,whiteSpace:"pre-wrap",wordBreak:"break-word"}}>{cm.body}</div>
      </div>)}
      {canEdit && <div style={{display:"flex",alignItems:"flex-end",gap:9,padding:"11px 14px",borderRadius:13,background:COLORS.card,border:`1px solid ${(cFocus||cbody)?COLORS.accentEdge:COLORS.hairline}`,margin:"10px 0 4px",transition:"border .15s"}}>
        <textarea value={cbody} onChange={e=>setCbody(e.target.value)} onFocus={()=>setCFocus(true)} onBlur={()=>setCFocus(false)}
          onKeyDown={e=>{ if((e.metaKey||e.ctrlKey)&&e.key==="Enter") addComment(); }}
          placeholder={comments.length?"ตอบกลับ… (@ชื่อ เพื่อแท็ก)":"เขียนคอมเมนต์…  (@ ชื่อ เพื่อแท็ก)"} rows={(cFocus||cbody)?2:1}
          style={{flex:1,background:"transparent",border:"none",outline:"none",color:COLORS.ink,fontSize:13.5,resize:"vertical",fontFamily:"inherit"}}/>
        {(cFocus||cbody.trim()) && <button disabled={busy||!cbody.trim()} onMouseDown={e=>e.preventDefault()} onClick={addComment} style={{padding:"8px 14px",borderRadius:10,background:cbody.trim()?COLORS.accent:COLORS.card,color:cbody.trim()?"var(--th-on-accent)":COLORS.inkMuted,border:`1px solid ${cbody.trim()?COLORS.accent:COLORS.hairline}`,fontWeight:700,fontSize:13,whiteSpace:"nowrap",cursor:cbody.trim()?"pointer":"default"}}>ส่ง</button>}
      </div>}
      {reviews.length>0 && <><div style={{height:1,background:COLORS.hairline,margin:"14px 0 12px"}}/>
        <div style={SUBHEAD}>โน้ตรีวิว <span style={ZCNT}>{reviews.length}</span></div>
        <div style={{display:"flex",flexDirection:"column",gap:8,marginBottom:4}}>
          {reviews.map(rv=><div key={rv.id} style={{padding:"11px 13px",background:COLORS.card,borderRadius:12,border:`1px solid ${rv.decision==="approved"?STATUS.done.dot:STATUS.blocked.dot}33`}}>
            <div style={{display:"flex",alignItems:"center",gap:7,marginBottom:rv.notes?6:0}}>
              <span style={{fontSize:12,fontWeight:700,color:rv.decision==="approved"?STATUS.done.fg:STATUS.blocked.fg}}>{rv.decision==="approved"?"✅ อนุมัติ":"↩️ ตีกลับ"}</span>
              <div style={{flex:1}}/>
              <span style={{fontSize:10,color:COLORS.inkMuted,fontFamily:MONO_FONT,letterSpacing:0.3}}>{rv.reviewer_name} · {(rv.created_at||"").slice(5,16).replace("T"," ")}</span>
            </div>
            {rv.notes && <div style={{fontSize:13,color:COLORS.ink,lineHeight:1.5}}>{rv.notes}</div>}
          </div>)}
        </div></>}
      {audit.length>0 && <><div style={{height:1,background:COLORS.hairline,margin:"14px 0 12px"}}/>
        <div style={SUBHEAD}>ความเคลื่อนไหว</div>
        <AuditTimeline events={audit}/></>}
      {discCount===0 && <div style={{fontSize:12.5,color:COLORS.inkSubtle,padding:"6px 0",fontStyle:"italic"}}>ยังไม่มีคอมเมนต์หรือความเคลื่อนไหว</div>}
    </div>}
  </div>;

  // ── edit form (เดิม · กางเมื่อ edit) ──
  const editForm = <div style={{margin:"12px 0 18px"}}>
    {(()=>{ const inp={width:"100%",padding:"10px 12px",borderRadius:10,background:COLORS.card,border:`1px solid ${COLORS.hairline}`,color:COLORS.ink,fontSize:14,marginBottom:8}; return <>
      <input style={inp} value={ef.title} onChange={e=>setEf({...ef,title:e.target.value})} placeholder="ชื่องาน"/>
      <input style={inp} type="date" value={ef.due} onChange={e=>setEf({...ef,due:e.target.value})}/>
      <input style={inp} value={ef.brand} onChange={e=>setEf({...ef,brand:e.target.value})} placeholder="แบรนด์"/>
      <select style={inp} value={ef.priority} onChange={e=>setEf({...ef,priority:e.target.value})}>{["P0","P1","P2","P3"].map(x=><option key={x} value={x}>{x}</option>)}</select>
      {canSeeAll && usersArr.length>0 && <select style={inp} value={ef.owner_id||""} onChange={e=>setEf({...ef,owner_id:e.target.value})}><option value="">— ไม่มีเจ้าของ —</option>{usersArr.map(u=><option key={u.id} value={u.id}>{u.name}</option>)}</select>}
      <select style={inp} value={ef.recurrence||""} onChange={e=>setEf({...ef,recurrence:e.target.value})}><option value="">ไม่ทำซ้ำ</option><option value="daily">🔁 ทุกวัน</option><option value="weekly">🔁 ทุกสัปดาห์</option></select>
      <input style={inp} value={ef.tags||""} onChange={e=>setEf({...ef,tags:e.target.value})} placeholder="แท็ก คั่นด้วย , (เช่น ด่วน, รีวิว)"/>
      <textarea style={{...inp,minHeight:88,resize:"vertical",fontFamily:"inherit",lineHeight:1.5}} value={ef.description||""} onChange={e=>setEf({...ef,description:e.target.value})} placeholder="รายละเอียดงาน… (วางลิงก์ Google Drive บรรทัดละ 1 เพื่อแทรกรูป/วิดีโอ)"/>
      <div style={{display:"flex",gap:6,marginBottom:10}}>
        <input value={mediaUrl} onChange={e=>setMediaUrl(e.target.value)} onKeyDown={e=>{ if(e.key==="Enter"){ e.preventDefault(); insertMedia(); } }} placeholder="วาง Google Drive link แล้วกดแทรก" style={{flex:1,padding:"10px 12px",borderRadius:10,background:COLORS.card,border:`1px solid ${COLORS.hairline}`,color:COLORS.ink,fontSize:13,minWidth:0}}/>
        <button type="button" onClick={insertMedia} style={{padding:"10px 14px",borderRadius:10,background:COLORS.accentSoft,color:COLORS.accentBright,border:`1px solid ${COLORS.accentEdge}`,fontWeight:700,whiteSpace:"nowrap",cursor:"pointer"}}>📎 แทรกสื่อ</button>
      </div>
      <div style={{display:"flex",gap:8}}>
        <button disabled={busy} onClick={saveEdit} style={{flex:1,padding:10,borderRadius:10,background:COLORS.accent,color:"var(--th-on-accent)",fontWeight:700,border:"none",cursor:"pointer"}}>บันทึก</button>
        <button onClick={()=>setEdit(false)} style={{flex:1,padding:10,borderRadius:10,background:COLORS.card,color:COLORS.inkMuted,border:`1px solid ${COLORS.hairline}`,cursor:"pointer"}}>ยกเลิก</button>
      </div></>; })()}
  </div>;

  // ── overflow ⋯ menu (destructive + leader rows) ──
  const ovfMenu = <div style={{position:"relative"}}>
    <button onClick={e=>{e.stopPropagation();setOvfOpen(o=>!o);setDocOpen(false);}} aria-label="เพิ่มเติม" title="เพิ่มเติม" style={{background:"transparent",border:"1px solid transparent",color:COLORS.inkMuted,fontSize:18,lineHeight:1,cursor:"pointer",padding:"5px 11px",borderRadius:10,fontWeight:600}}>⋯</button>
    {ovfOpen && <div onClick={e=>e.stopPropagation()} style={{position:"absolute",top:"calc(100% + 8px)",right:0,minWidth:210,zIndex:20,background:COLORS.cardElev,border:`1px solid ${COLORS.hairlineStr}`,borderRadius:14,padding:6,boxShadow:"var(--th-shadow-lg)"}}>
      {(()=>{ const mItem={display:"flex",alignItems:"center",gap:10,width:"100%",textAlign:"left",background:"transparent",border:"none",cursor:"pointer",fontSize:13,fontWeight:600,color:COLORS.ink,padding:"9px 11px",borderRadius:9};
        const mi={width:18,textAlign:"center",color:COLORS.inkMuted};
        const dItem={...mItem,color:"var(--th-st-red-fg)"}; const di2={...mi,color:"var(--th-st-red-fg)"};
        return <>
          {canEdit && <button onClick={()=>{setOvfOpen(false);startEdit();}} style={mItem}><span style={mi}>✎</span> แก้ไขรายละเอียด</button>}
          {canEdit && [["พรุ่งนี้",1],["+3 วัน",3],["+1 สัปดาห์",7]].map(([lb,dn])=><button key={dn} disabled={busy} onClick={()=>{setOvfOpen(false);rescheduleDays(dn);}} style={mItem}><span style={mi}>📅</span> เลื่อนกำหนด · {lb}</button>)}
          {isLeader && canSeeAll && usersArr.length>0 && <button onClick={()=>{setOvfOpen(false);setPickOwner(true);}} style={mItem}><span style={mi}>👤</span> มอบหมายใหม่</button>}
          {(canEdit||canDelete) && <div style={{height:1,background:COLORS.hairline,margin:"5px 7px"}}/>}
          {canEdit && t.status!=="cancelled" && <button disabled={busy} onClick={()=>{setOvfOpen(false);cancelTask();}} style={dItem}><span style={di2}>🚫</span> ยกเลิกงาน</button>}
          {canDelete && <button disabled={busy} onClick={()=>{setOvfOpen(false);deleteTask();}} style={dItem}><span style={di2}>🗑</span> ลบงาน</button>}
        </>; })()}
    </div>}
  </div>;

  // ── header bar ──
  const headerBar = <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:18,flexWrap:"wrap"}}>
    <Badge bg={p.soft} fg={p.color}>{p.label}</Badge>
    <StatusPill s={t.status}/>
    <div style={{flex:1}}/>
    {canEdit && !edit && <button onClick={startEdit} style={{background:"transparent",border:"1px solid transparent",color:COLORS.accentBright,fontSize:13,fontWeight:600,cursor:"pointer",padding:"7px 11px",borderRadius:10}}>✎ แก้ไข</button>}
    {(canEdit||canDelete) && ovfMenu}
  </div>;

  // ── meta strip ──
  const sep=<span style={{color:COLORS.inkFaint,margin:"0 8px",userSelect:"none"}}>·</span>;
  const metaStrip = <div style={{display:"flex",flexWrap:"wrap",alignItems:"center",gap:"2px 4px",marginBottom:16,fontSize:13.5,color:COLORS.inkMuted}}>
    <span style={{display:"inline-flex",alignItems:"center",gap:6,fontWeight:600}}>👤 {ownerName}
      {isLeader && canSeeAll && usersArr.length>0 && <button onClick={()=>setPickOwner(p=>!p)} title="มอบหมายใหม่" style={{background:"transparent",border:"none",cursor:"pointer",fontFamily:MONO_FONT,fontSize:10,fontWeight:700,color:COLORS.accentBright,letterSpacing:0.4}}>▾</button>}
    </span>{sep}
    {t.brand && <><span style={{display:"inline-flex",alignItems:"center",gap:6,fontWeight:600}}>🏷 {t.brand}</span>{sep}</>}
    <span style={{display:"inline-flex",alignItems:"center",gap:6,fontWeight:600}}>📅 {di?<span style={{color:di.color}}>{di.text}</span>:"—"}</span>{sep}
    <span style={{color:"var(--th-st-orange-fg)",background:"var(--th-st-orange-bg)",padding:"2px 10px",borderRadius:999,fontFamily:MONO_FONT,fontWeight:700,fontSize:11.5}}>{t.priority||"—"}</span>
    {isLeader && t.recurrence && <>{sep}<span style={{display:"inline-flex",alignItems:"center",gap:6,fontWeight:600}}>🔁 {RECUR_LABEL[t.recurrence]}</span></>}
  </div>;

  // owner reassign select (leader · เปิดจาก ▾ หรือ menu)
  const ownerPicker = pickOwner && canSeeAll && usersArr.length>0 && <div style={{display:"flex",gap:8,alignItems:"center",marginBottom:16}}>
    <span style={{fontSize:12,color:COLORS.inkMuted,fontWeight:600}}>มอบหมายให้</span>
    <select value={t.owner_id||""} onChange={async e=>{ const v=e.target.value; setPickOwner(false); if(await gate({title:"มอบหมายงานใหม่?",body:`"${t.title}" → ${(usersArr.find(u=>String(u.id)===v)||{}).name||"ไม่มีเจ้าของ"}`})) patch({owner_id:v?+v:null}); }} style={{flex:1,padding:"8px 10px",borderRadius:10,background:COLORS.card,border:`1px solid ${COLORS.hairline}`,color:COLORS.ink,fontSize:13}}><option value="">— ไม่มีเจ้าของ —</option>{usersArr.map(u=><option key={u.id} value={u.id}>{u.name}</option>)}</select>
    <button onClick={()=>setPickOwner(false)} style={{padding:"8px 11px",borderRadius:10,background:"transparent",color:COLORS.inkMuted,border:`1px solid ${COLORS.hairline}`,cursor:"pointer"}}>✕</button>
  </div>;

  // ── ส่วนหัว (title + meta + desc) ที่ใช้ร่วมทั้ง embedded/modal ──
  const headSection = <>
    <div style={{fontSize:embedded?25:21,fontWeight:700,color:COLORS.inkBright,lineHeight:1.28,letterSpacing:0.1,marginBottom:11}}>{t.title}</div>
    {metaStrip}
    {ownerPicker}
    {t.description && <div style={{fontSize:14,color:COLORS.ink,lineHeight:1.62,marginBottom:22,paddingLeft:14,borderLeft:`2px solid ${COLORS.accentEdge}`,maxWidth:"64ch"}}><MediaRenderer text={t.description}/></div>}
    {/* tags display ถูกเอาออก — ไม่มีประโยชน์ (คุณเอก 23 มิ.ย.) · field tags ยังอยู่ใน edit/data */}
    <ExtraView raw={t.extra}/>
  </>;

  const content = (<div onClick={()=>{setOvfOpen(false);setDocOpen(false);}}>
        {!embedded && <div style={{width:40,height:4,background:COLORS.hairlineStr,borderRadius:9,margin:"0 auto 18px"}}/>}
        {headerBar}
        {edit ? editForm : <>
          {headSection}
          {/* 2 โซน desktop · 1 คอลัมน์ mobile (CSS class th-task-grid · breakpoint 760px ใน index.html) */}
          <div className="th-task-grid" style={{display:"grid",gridTemplateColumns:embedded?"1.18fr .82fr":"1fr",gap:18,alignItems:"start"}}>
            <div>{zoneAction}</div>
            <div style={{display:"flex",flexDirection:"column",gap:16}}>
              {zoneChecklist}
              {zoneReview}
              {zoneDiscuss}
            </div>
          </div>
        </>}
        <button onClick={onClose} style={{width:"100%",marginTop:18,padding:13,borderRadius:14,background:COLORS.card,color:COLORS.ink,border:`1px solid ${COLORS.hairline}`,fontWeight:600,cursor:"pointer"}}>{embedded?"ล้างที่เลือก":"ปิด"}</button>
        {focusOn && <FocusOverlay t={t} startedAt={focusStart} clist={clist} done={focusDone}
          onToggle={(i)=>saveClist(clist.map((x,j)=>j===i?{...x,done:!x.done}:x))}
          onDone={focusComplete} onSnooze={focusSnooze} onExit={()=>{setFocusOn(false);setFocusDone(null);}}/>}
  </div>);
  if(embedded) return <div style={{background:COLORS.bgElev,borderRadius:18,padding:22,border:`1px solid ${COLORS.hairline}`}}>{content}</div>;
  return (
    <div style={{position:"fixed",inset:0,background:"var(--th-overlay)",display:"flex",alignItems:"flex-end",justifyContent:"center",zIndex:50}} onClick={onClose}>
      <div onClick={e=>e.stopPropagation()} style={{width:"100%",maxWidth:520,background:COLORS.bgElev,borderRadius:"22px 22px 0 0",padding:22,maxHeight:"88vh",overflowY:"auto"}}>
        {content}
      </div>
    </div>
  );
}

// ---- Change password ----
function ChangePw({onClose}){
  const [o,setO]=useState(""); const [n,setN]=useState(""); const [n2,setN2]=useState(""); const [msg,setMsg]=useState(""); const [busy,setBusy]=useState(false);
  async function submit(){ setMsg(""); if(n!==n2){setMsg("รหัสใหม่ไม่ตรงกัน");return;} if(n.length<6){setMsg("อย่างน้อย 6 ตัว");return;}
    setBusy(true); try{ await api("POST","/api/auth/change-password",{old_password:o,new_password:n}); setMsg("✓ เปลี่ยนรหัสแล้ว"); setTimeout(onClose,900); }
    catch(e){ setMsg("รหัสเดิมไม่ถูกต้อง"); } finally{ setBusy(false); } }
  const inp={width:"100%",padding:"12px 14px",borderRadius:12,background:COLORS.card,border:`1px solid ${COLORS.hairline}`,color:COLORS.ink,fontSize:15,marginBottom:10};
  const isD=useIsDesktop(); useEscClose(onClose);
  return (
    <div style={{position:"fixed",inset:0,background:"var(--th-overlay)",display:"flex",alignItems:isD?"center":"flex-end",justifyContent:"center",zIndex:60,padding:isD?20:0}} onClick={onClose}>
      <div onClick={e=>e.stopPropagation()} style={{width:"100%",maxWidth:440,background:COLORS.bgElev,borderRadius:isD?18:"22px 22px 0 0",padding:22,border:isD?`1px solid ${COLORS.hairline}`:"none"}}>
        {!isD && <div style={{width:40,height:4,background:COLORS.hairlineStr,borderRadius:9,margin:"0 auto 18px"}}/>}
        <div style={{fontSize:19,fontWeight:700,marginBottom:16}}>เปลี่ยนรหัสผ่าน</div>
        <input style={inp} type="password" placeholder="รหัสผ่านเดิม" value={o} onChange={e=>setO(e.target.value)}/>
        <input style={inp} type="password" placeholder="รหัสใหม่ (≥6 ตัว)" value={n} onChange={e=>setN(e.target.value)}/>
        <input style={inp} type="password" placeholder="ยืนยันรหัสใหม่" value={n2} onChange={e=>setN2(e.target.value)}/>
        {msg && <div style={{color:msg.startsWith("✓")?STATUS.done.fg:"var(--th-st-red-fg)",fontSize:13,marginBottom:10}}>{msg}</div>}
        <button disabled={busy} onClick={submit} style={{width:"100%",padding:14,borderRadius:14,background:COLORS.accent,color:"#fff",fontWeight:700,opacity:busy?.6:1}}>{busy?"กำลังบันทึก...":"บันทึก"}</button>
      </div>
    </div>
  );
}

// ---- Create ----
function Create({pipelines, users, onClose, onCreated, canSeeAll, defPipeline, defDue, defOwner}){
  const [f,setF]=useState({title:"",pipeline_key:defPipeline||pipelines[0]?.key||"",priority:"P1",brand:"",due:defDue||"",owner_id:defOwner||"",recurrence:"",tags:"",description:""});
  const [busy,setBusy]=useState(false); const [adv,setAdv]=useState(false);
  const up=(k,v)=>setF(s=>({...s,[k]:v}));
  async function submit(){ if(!f.title.trim()){return;} setBusy(true);
    // parse token ในชื่อ (!P0 / พรุ่งนี้ / วันที่) ให้เหมือน quick-add · token ที่พิมพ์ชนะ dropdown · ชื่อถูกล้างให้สะอาด
    const q=parseQuick(f.title);
    try{ await api("POST","/api/tasks",{title:q.title||f.title.trim(),pipeline_key:f.pipeline_key,
      priority:q.priority||f.priority, due:q.due||f.due||null,
      owner_id:f.owner_id?+f.owner_id:null, brand:f.brand||null, recurrence:q.recurrence||f.recurrence||null, tags:f.tags||null, description:f.description||null}); onCreated(); onClose(); }
    catch(e){alert(e.message);} finally{setBusy(false);} }
  const inp={width:"100%",padding:"12px 14px",borderRadius:12,background:COLORS.card,border:`1px solid ${COLORS.hairline}`,color:COLORS.ink,fontSize:15,marginBottom:10};
  const isD=useIsDesktop(); useEscClose(onClose);
  return (
    <div style={{position:"fixed",inset:0,background:"var(--th-overlay)",display:"flex",alignItems:isD?"center":"flex-end",justifyContent:"center",zIndex:50,padding:isD?20:0}} onClick={onClose}>
      <div onClick={e=>e.stopPropagation()} style={{width:"100%",maxWidth:480,background:COLORS.bgElev,borderRadius:isD?18:"22px 22px 0 0",padding:22,maxHeight:"88vh",overflowY:"auto",border:isD?`1px solid ${COLORS.hairline}`:"none"}}>
        {!isD && <div style={{width:40,height:4,background:COLORS.hairlineStr,borderRadius:9,margin:"0 auto 18px"}}/>}
        <div style={{fontSize:19,fontWeight:700,marginBottom:16}}>เพิ่มงานใหม่</div>
        <input style={inp} placeholder="ชื่องาน (พิมพ์ !P0 / พรุ่งนี้ ได้)" value={f.title} onChange={e=>up("title",e.target.value)} autoFocus/>
        <select style={inp} value={f.pipeline_key} onChange={e=>up("pipeline_key",e.target.value)}>{pipelines.map(p=><option key={p.key} value={p.key}>{p.name}</option>)}</select>
        <div style={{display:"flex",gap:10}}>
          <select style={{...inp,flex:1}} value={f.priority} onChange={e=>up("priority",e.target.value)}>{["P0","P1","P2","P3"].map(p=><option key={p} value={p}>{p}</option>)}</select>
          <input style={{...inp,flex:1}} type="date" value={f.due} onChange={e=>up("due",e.target.value)}/>
        </div>
        {canSeeAll
          ? <select style={inp} value={f.owner_id} onChange={e=>up("owner_id",e.target.value)}><option value="">— ผู้รับผิดชอบ —</option>{users.map(u=><option key={u.id} value={u.id}>{u.name}</option>)}</select>
          : <div style={{...inp,color:COLORS.inkMuted,display:"flex",alignItems:"center"}}>👤 มอบให้: ตัวคุณเอง</div>}
        <div onClick={()=>setAdv(a=>!a)} style={{fontSize:13,color:COLORS.accentBright,cursor:"pointer",fontWeight:600,padding:"4px 2px",marginBottom:8,userSelect:"none"}}>{adv?"▾":"▸"} รายละเอียดเพิ่มเติม <span style={{color:COLORS.inkSubtle,fontWeight:400}}>(แบรนด์ · ทำซ้ำ · แท็ก · คำอธิบาย)</span></div>
        {adv && <>
          <input style={inp} placeholder="แบรนด์ (ไม่บังคับ)" value={f.brand} onChange={e=>up("brand",e.target.value)}/>
          <select style={inp} value={f.recurrence} onChange={e=>up("recurrence",e.target.value)}><option value="">ไม่ทำซ้ำ</option><option value="daily">🔁 ทุกวัน</option><option value="weekly">🔁 ทุกสัปดาห์</option></select>
          <input style={inp} value={f.tags} onChange={e=>up("tags",e.target.value)} placeholder="แท็ก คั่นด้วย , (ไม่บังคับ)"/>
          <textarea style={{...inp,minHeight:80,resize:"vertical",fontFamily:"inherit",lineHeight:1.5}} value={f.description} onChange={e=>up("description",e.target.value)} placeholder="รายละเอียด… วางลิงก์ Google Drive เพื่อแทรกรูป/วิดีโอ (ไม่บังคับ)"/>
        </>}
        <button disabled={busy} onClick={submit} style={{width:"100%",padding:14,borderRadius:14,background:COLORS.accent,color:"#fff",fontWeight:700,fontSize:15,opacity:busy?.6:1}}>{busy?"กำลังบันทึก...":"สร้างงาน"}</button>
      </div>
    </div>
  );
}

// ---- Board (main) ----
// Admin Overview dashboard — match design (KPI · trend · workload · pipeline health · activity)
function Overview({onOpenTask, onPickOwner, onKpi, meId}){
  const isDesktop=useIsDesktop();
  const [d,setD]=React.useState(null);
  React.useEffect(()=>{ api("GET","/api/overview").then(setD).catch(()=>setD({})); },[]);
  if(!d) return <div style={{color:COLORS.inkMuted,textAlign:"center",padding:50}}>กำลังโหลดภาพรวม...</div>;
  const k=d.kpi||{}, pipes=d.by_pipeline||[], trend=d.trend||[], wl=d.workload||[], acts=d.activity||[];
  const maxW=Math.max(1,...wl.map(w=>w.open||0));
  const Card=({title,count,action,children,pad=true})=>(
    <div style={{background:COLORS.card,borderRadius:14,border:`1px solid ${COLORS.hairline}`,overflow:"hidden"}}>
      {title&&<div style={{padding:"13px 16px 10px",display:"flex",alignItems:"center",gap:8,borderBottom:`1px solid ${COLORS.hairlineSoft}`}}>
        <span style={{fontSize:10.5,fontWeight:700,color:COLORS.accentBright,letterSpacing:1.5,fontFamily:MONO_FONT,textTransform:"uppercase"}}>{title}</span>
        {count!=null&&<span style={{fontFamily:NUM_FONT,fontSize:11,fontWeight:700,color:COLORS.inkMuted,background:"var(--th-row-soft-2)",padding:"1px 7px",borderRadius:999}}>{count}</span>}
        <div style={{flex:1}}/>
        {action&&<span style={{fontSize:11.5,color:COLORS.accentBright,fontWeight:600}}>{action}</span>}
      </div>}
      <div style={{padding:pad?16:0}}>{children}</div>
    </div>
  );
  const KPI=({label,value,unit,tint,sub,accent,kind})=>(
    <div onClick={()=>kind&&onKpi&&onKpi(kind)} style={{flex:"1 1 130px",minWidth:118,padding:"15px 17px",background:accent?`linear-gradient(135deg, ${COLORS.accentSoft} 0%, ${COLORS.card} 70%)`:COLORS.card,borderRadius:14,border:`1px solid ${accent?COLORS.accentEdge:COLORS.hairline}`,cursor:kind?"pointer":"default",transition:"border .15s"}}>
      <div style={{fontSize:10,color:COLORS.inkMuted,fontWeight:700,letterSpacing:1.2,fontFamily:MONO_FONT,textTransform:"uppercase",whiteSpace:"nowrap",display:"flex",alignItems:"center",gap:5}}>{label}{kind&&<span style={{color:COLORS.inkSubtle,fontSize:11}}>›</span>}</div>
      <div style={{display:"flex",alignItems:"baseline",gap:5,marginTop:8}}>
        <span style={{fontFamily:NUM_FONT,fontSize:32,fontWeight:700,color:tint||COLORS.inkBright,lineHeight:1,letterSpacing:-0.8,textShadow:tint?`0 0 18px ${tint}40`:"none"}}>{value}</span>
        {unit&&<span style={{fontSize:13,color:COLORS.inkMuted}}>{unit}</span>}
      </div>
      {sub&&<div style={{fontSize:11,color:tint||COLORS.inkMuted,fontWeight:600,marginTop:7,whiteSpace:"nowrap"}}>{sub}</div>}
    </div>
  );
  const Trend=()=>{ const W=600,H=170,mx=Math.max(1,...trend)*1.18;
    const pts=trend.map((v,i)=>[(i/Math.max(1,trend.length-1))*(W-30)+15, H-16-(v/mx)*(H-32)]);
    const path=pts.map(([x,y],i)=>(i?"L":"M")+x.toFixed(1)+" "+y.toFixed(1)).join(" ");
    return <svg width="100%" viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" style={{display:"block"}}>
      <defs><linearGradient id="ovtg" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor="#7C5BFF" stopOpacity="0.5"/><stop offset="100%" stopColor="#7C5BFF" stopOpacity="0"/></linearGradient></defs>
      {[0.25,0.5,0.75].map((p,i)=><line key={i} x1="15" x2={W-15} y1={H-16-p*(H-32)} y2={H-16-p*(H-32)} stroke={COLORS.hairlineSoft} strokeWidth="1" strokeDasharray="2 4"/>)}
      <path d={path+` L ${W-15} ${H-16} L 15 ${H-16} Z`} fill="url(#ovtg)"/>
      <path d={path} fill="none" stroke="#B89FFF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
      {pts.map(([x,y],i)=>{const last=i===pts.length-1; return <g key={i}>
        <circle cx={x} cy={y} r={last?5:2.5} fill={last?"#fff":"#B89FFF"} stroke={last?"#B89FFF":"transparent"} strokeWidth="2"/>
        {last&&<text x={x} y={y-12} fontSize="12" fontWeight="700" fill="#fff" textAnchor="middle">{trend[i]}</text>}
        <text x={x} y={H-3} fontSize="9" fontFamily={MONO_FONT} fill={COLORS.inkSubtle} textAnchor="middle">W{i+1}</text>
      </g>;})}
    </svg>;
  };
  const ACT={created:["สร้าง","var(--th-st-gray-fg)"],status:["อัปเดต","#5BA0FF"],handoff:["ส่งต่อ","var(--th-st-orange-fg)"],assigned:["มอบงาน","#B47EFF"],add_link:["ลิงก์","#56C8D8"],review:["รีวิว","#3CD680"],update:["แก้ไข","#9B7EFF"]};
  const left=<>
    <Card title="งานเสร็จ · 8 สัปดาห์" action="throughput จริง">
      <div style={{display:"flex",alignItems:"baseline",gap:10,marginBottom:8}}>
        <span style={{fontFamily:NUM_FONT,fontSize:28,fontWeight:700,color:COLORS.inkBright,lineHeight:1}}>{trend.reduce((a,b)=>a+b,0)}</span>
        <span style={{fontSize:12,color:COLORS.inkMuted}}>งานเสร็จ · 8 สัปดาห์</span>
      </div><Trend/>
    </Card>
    <Card title="ใครงานล้น · workload" action="คลิกดูงานรายคน">
      {wl.length===0&&<div style={{color:COLORS.inkSubtle,fontSize:13}}>—</div>}
      {wl.map((w,i)=>{const pct=Math.round((w.open||0)/maxW*100),bar=w.open>=10?"var(--th-st-red-dot)":w.open>=6?"var(--th-st-orange-fg)":"var(--th-st-green-dot)";
        return <div key={i} onClick={()=>w.id&&onPickOwner&&onPickOwner(w.id,w.name)} style={{display:"flex",alignItems:"center",gap:12,padding:"8px 0",cursor:w.id?"pointer":"default",borderRadius:8}}>
          <div style={{width:108,minWidth:0}}>
            <div style={{fontSize:12.5,fontWeight:700,color:w.id===meId?COLORS.accentBright:COLORS.inkBright,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"}}>{w.name}{w.id===meId?" (คุณ)":""}</div>
            <div style={{fontSize:9.5,color:COLORS.inkSubtle,fontFamily:MONO_FONT,marginTop:2}}>{w.role}</div></div>
          <div style={{flex:1,height:8,borderRadius:999,background:"var(--th-row-soft-2)",position:"relative"}}>
            <div style={{position:"absolute",top:0,left:0,bottom:0,width:pct+"%",background:`linear-gradient(90deg, ${bar}80, ${bar})`,borderRadius:999}}/></div>
          <span style={{fontFamily:NUM_FONT,fontSize:14,fontWeight:700,color:COLORS.inkBright,width:22,textAlign:"right"}}>{w.open}</span>
        </div>;})}
    </Card></>;
  const right=<>
    <Card title="Pipeline health" pad={false}>
      {pipes.map((p,i)=>{const tk=pipeTok(p.key); return <div key={p.key} style={{display:"flex",alignItems:"center",gap:12,padding:"10px 16px",borderBottom:i<pipes.length-1?`1px solid ${COLORS.hairlineSoft}`:"none"}}>
        <span style={{width:9,height:9,borderRadius:999,background:tk.color,boxShadow:`0 0 8px ${tk.color}`,flexShrink:0}}/>
        <span style={{fontSize:13,fontWeight:600,color:COLORS.ink,flex:1,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"}}>{tk.label}</span>
        {p.late>0&&<span style={{fontSize:9.5,fontWeight:700,color:"var(--th-st-red-fg)",background:"var(--th-st-red-bg)",padding:"2px 6px",borderRadius:4,fontFamily:MONO_FONT}}>LATE {p.late}</span>}
        <span style={{fontFamily:NUM_FONT,fontSize:15,fontWeight:700,color:COLORS.inkBright}}>{p.c}<span style={{fontSize:10,color:COLORS.inkSubtle,fontWeight:500,marginLeft:3}}>OPEN</span></span>
      </div>;})}
    </Card>
    <Card title="กิจกรรมล่าสุด · activity" count={acts.length} pad={false}>
      {acts.length===0&&<div style={{padding:16,color:COLORS.inkSubtle,fontSize:13}}>ยังไม่มีกิจกรรม</div>}
      {acts.map((a,i)=>{const tk=ACT[a.action]||[a.action,"var(--th-st-gray-fg)"]; return <div key={i} onClick={()=>a.task_id&&onOpenTask&&onOpenTask(a.task_id)} style={{display:"flex",gap:10,alignItems:"flex-start",padding:"10px 16px",borderBottom:i<acts.length-1?`1px solid ${COLORS.hairlineSoft}`:"none",cursor:a.task_id?"pointer":"default"}}>
        <span style={{width:7,height:7,borderRadius:999,background:tk[1],marginTop:5,flexShrink:0,boxShadow:`0 0 6px ${tk[1]}`}}/>
        <div style={{flex:1,minWidth:0}}>
          <div style={{fontSize:13,color:COLORS.ink,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}><span style={{color:tk[1],fontWeight:700,fontSize:11}}>{tk[0]} </span>{fmtAudit(a)}{a.task_title?" · "+a.task_title:""}</div>
          <div style={{fontSize:11,color:COLORS.inkMuted,marginTop:2}}>{a.by_name||"—"} · {(a.created_at||"").slice(5,16).replace("T"," ")}</div>
        </div>
      </div>;})}
    </Card></>;
  return <div style={{maxWidth:1180,margin:"0 auto",padding:"10px 18px 30px"}}>
    <div style={{display:"flex",gap:12,flexWrap:"wrap",marginBottom:16}}>
      <KPI label="งานเปิดอยู่" value={k.open||0} sub="ค้างใน pipeline" kind="all"/>
      <KPI label="กำลังทำ" value={k.in_progress||0} tint="#7AB2FF" sub="in progress" kind="all"/>
      <KPI label="เลยกำหนด" value={k.overdue||0} tint="var(--th-st-red-fg)" sub="กดดูเลย" kind="overdue"/>
      <KPI label="เสร็จ 7 วัน" value={k.done7||0} tint="#6EE5A0" sub="throughput" accent/>
      <KPI label="คนมีงาน" value={k.team||0} sub="active"/>
    </div>
    <div style={{display:"grid",gridTemplateColumns:isDesktop?"1.6fr 1fr":"1fr",gap:14}}>
      <div style={{display:"flex",flexDirection:"column",gap:14,minWidth:0}}>{left}</div>
      <div style={{display:"flex",flexDirection:"column",gap:14,minWidth:0}}>{right}</div>
    </div>
  </div>;
}

// วันนี้ (เวลาไทย) สำหรับ default due
const bkkToday=()=>{ try{ return new Intl.DateTimeFormat('en-CA',{timeZone:'Asia/Bangkok'}).format(new Date()); }catch(_){ return new Date().toISOString().slice(0,10); } };

// Quick-add ในคอลัมน์ Kanban — พิมพ์ชื่อ → Enter เพิ่มทันที (default: วันนี้/P1/ฉัน/stage นี้) · ▸ กาง Advanced
function ColQuickAdd({stageId,pipelineKey,brand,user,users,canSeeAll,onAdded,onClose}){
  const [title,setTitle]=React.useState(""); const [adv,setAdv]=React.useState(false); const [busy,setBusy]=React.useState(false);
  const [priority,setPriority]=React.useState("P1"); const [due,setDue]=React.useState(bkkToday());
  const [ownerId,setOwnerId]=React.useState(user&&user.id?user.id:""); const [tags,setTags]=React.useState("");
  const inRef=React.useRef(null);
  React.useEffect(()=>{ if(inRef.current) inRef.current.focus(); },[]);
  async function submit(){ const q=parseQuick(title); const t=q.title; if(!t||busy) return; setBusy(true);
    try{ await api("POST","/api/tasks",{title:t,pipeline_key:pipelineKey,current_stage_id:stageId,priority:q.priority||priority,due:q.due||due||undefined,owner_id:ownerId||undefined,tags:tags.trim()||undefined,brand:brand||undefined,recurrence:q.recurrence||undefined});
      setTitle(""); onAdded&&onAdded(); if(inRef.current) inRef.current.focus();   // เปิดค้าง พิมพ์ต่อได้รัวๆ
    }catch(e){ alert(e.message); } finally{ setBusy(false); } }
  const inp={width:"100%",padding:"7px 9px",borderRadius:8,background:COLORS.card,border:`1px solid ${COLORS.hairline}`,color:COLORS.ink,fontSize:12.5,marginBottom:6,boxSizing:"border-box"};
  const ownerName=canSeeAll?((((users||[]).find(u=>String(u.id)===String(ownerId))||{}).name)||"ไม่มีเจ้าของ"):"ฉัน";
  return <div style={{border:`1px solid ${COLORS.accentEdge}`,borderRadius:10,padding:8,background:"var(--th-hover-accent)"}}>
    <input ref={inRef} value={title} onChange={e=>setTitle(e.target.value)}
      onKeyDown={e=>{ if(e.key==="Enter"){e.preventDefault();submit();} else if(e.key==="Escape"){onClose&&onClose();} }}
      placeholder="ชื่องาน… (Enter)" style={inp}/>
    <div onClick={()=>setAdv(a=>!a)} style={{fontSize:10.5,color:COLORS.inkMuted,cursor:"pointer",marginBottom:adv?8:6,userSelect:"none"}}>
      {adv?"▾":"▸"} {priority} · {due===bkkToday()?"วันนี้":(due||"ไม่กำหนด")} · {ownerName}</div>
    {adv && <div style={{display:"flex",flexDirection:"column",gap:6,marginBottom:8}}>
      <select value={priority} onChange={e=>setPriority(e.target.value)} style={inp}>{["P0","P1","P2","P3"].map(p=><option key={p} value={p}>{p}</option>)}</select>
      <input type="date" value={due} onChange={e=>setDue(e.target.value)} style={inp}/>
      {canSeeAll && <select value={ownerId} onChange={e=>setOwnerId(e.target.value)} style={inp}><option value="">— ไม่มีเจ้าของ —</option>{(users||[]).map(u=><option key={u.id} value={u.id}>{u.name}</option>)}</select>}
      <input value={tags} onChange={e=>setTags(e.target.value)} placeholder="แท็ก (คั่นด้วย ,)" style={{...inp,marginBottom:0}}/>
    </div>}
    <div style={{display:"flex",gap:6}}>
      <button onClick={submit} disabled={busy||!title.trim()} style={{flex:1,padding:"7px",borderRadius:8,background:COLORS.accent,color:"#fff",fontWeight:700,fontSize:12,cursor:"pointer",border:"none",opacity:(busy||!title.trim())?0.5:1}}>{busy?"…":"เพิ่ม"}</button>
      <button onClick={()=>onClose&&onClose()} style={{padding:"7px 10px",borderRadius:8,background:"transparent",border:`1px solid ${COLORS.hairline}`,color:COLORS.inkMuted,fontSize:12,cursor:"pointer"}}>ปิด</button>
    </div>
  </div>;
}

// Kanban board — columns = stages · cards grouped by current_stage_id (สำหรับ pipeline has_stages)
function Kanban({pipelineKey,onOpenTask,canEdit,user,users,canSeeAll}){
  const [stages,setStages]=React.useState(null); const [tasks,setTasks]=React.useState([]); const [brand,setBrand]=React.useState("all");
  const [dragId,setDragId]=React.useState(null); const [overId,setOverId]=React.useState(null); const [busy,setBusy]=React.useState(false);
  const [addCol,setAddCol]=React.useState(null);   // stage id ที่เปิด quick-add composer
  const [editId,setEditId]=React.useState(null); const [editVal,setEditVal]=React.useState(""); const [dateId,setDateId]=React.useState(null);   // #1 inline edit
  const isDesktop=useIsDesktop();   // มือถือ: kanban ปัดทีละคอลัมน์ (snap)
  const loadTasks=React.useCallback(()=>{ api("GET","/api/tasks?pipeline="+pipelineKey).then(t=>setTasks(t||[])).catch(()=>{}); },[pipelineKey]);
  React.useEffect(()=>{
    api("GET","/api/stages?pipeline="+pipelineKey).then(setStages).catch(()=>setStages([]));
    loadTasks();
  },[pipelineKey,loadTasks]);
  async function move(taskId,stageId){ const t=tasks.find(x=>x.id===taskId); if(!t||t.current_stage_id===stageId)return;
    setBusy(true);
    setTasks(ts=>ts.map(x=>x.id===taskId?{...x,current_stage_id:stageId}:x));  // optimistic
    try{ await api("PATCH","/api/tasks/"+taskId,{current_stage_id:stageId}); loadTasks(); }
    catch(e){ alert(e.message); loadTasks(); } finally{ setBusy(false); } }
  async function patchTask(id,body){ setTasks(ts=>ts.map(x=>x.id===id?{...x,...body}:x)); try{ await api("PATCH","/api/tasks/"+id,body); loadTasks(); }catch(e){ alert(e.message); loadTasks(); } }
  const cyclePri=(t)=>{ const o=["P0","P1","P2","P3"]; patchTask(t.id,{priority:o[(o.indexOf(t.priority)+1)%4]}); };
  if(!stages) return <div style={{color:COLORS.inkMuted,textAlign:"center",padding:40}}>กำลังโหลด board...</div>;
  if(!stages.length) return <div style={{color:COLORS.inkMuted,textAlign:"center",padding:40}}>pipeline นี้ไม่มี stage</div>;
  const byStage={}; stages.forEach(s=>byStage[s.id]=[]); const firstId=stages[0].id;
  const brands=[...new Set(tasks.map(t=>t.brand).filter(Boolean))];
  const ftasks=brand==="all"?tasks:tasks.filter(t=>t.brand===brand);
  ftasks.forEach(t=>{ const sid=(t.current_stage_id&&byStage[t.current_stage_id])?t.current_stage_id:firstId; byStage[sid].push(t); });
  const SC=["#9B7EFF","#5BA0FF","#56C8D8","var(--th-st-orange-fg)","#F4CC4E","#3CD680","#6EE5A0"];
  return <div style={{padding:"8px 18px 24px"}}>
    {brands.length>1 && <div style={{display:"flex",gap:8,marginBottom:12,flexWrap:"wrap"}}>
      {["all",...brands].map(b=>{const on=brand===b; return <button key={b} onClick={()=>setBrand(b)} style={{padding:"5px 13px",borderRadius:999,fontSize:12.5,fontWeight:700,cursor:"pointer",border:`1px solid ${on?COLORS.accent:COLORS.hairline}`,background:on?COLORS.accentSoft:"transparent",color:on?COLORS.accentBright:COLORS.inkMuted}}>{b==="all"?"📑 ทุกเพจ":b}</button>;})}
    </div>}
    {canEdit && <div style={{fontSize:11,color:COLORS.inkSubtle,marginBottom:8}}>💡 ลากการ์ดข้ามคอลัมน์เพื่อเลื่อนขั้น</div>}
    <div style={{position:"relative"}}>
    {stages.length>4 && <div style={{position:"absolute",top:0,right:0,bottom:8,width:40,pointerEvents:"none",zIndex:2,background:`linear-gradient(90deg,transparent,${COLORS.bgElev})`}}/>}
    <div style={{display:"flex",gap:10,alignItems:"flex-start",minWidth:"min-content",overflowX:"auto",scrollSnapType:isDesktop?undefined:"x mandatory",paddingBottom:8}}>
      {stages.map((s,i)=>{const items=byStage[s.id]||[],col=SC[i%SC.length],hot=overId===s.id&&canEdit;
        return <div key={s.id}
          onDragOver={canEdit?(e=>{e.preventDefault();setOverId(s.id);}):undefined}
          onDragLeave={canEdit?(()=>setOverId(o=>o===s.id?null:o)):undefined}
          onDrop={canEdit?(()=>{const id=dragId;setOverId(null);setDragId(null);if(id)move(id,s.id);}):undefined}
          style={{flex:isDesktop?"1 1 240px":"0 0 88%",minWidth:isDesktop?220:0,maxWidth:isDesktop?340:"none",scrollSnapAlign:isDesktop?undefined:"start",background:hot?"var(--th-hover-accent)":"var(--th-hover-2)",border:`1px solid ${hot?COLORS.accentEdge:COLORS.hairline}`,borderRadius:12,padding:9,display:"flex",flexDirection:"column",gap:8,transition:"background .12s"}}>
          <div style={{display:"flex",alignItems:"center",gap:8,padding:"2px 4px 8px",borderBottom:`1px solid ${COLORS.hairlineSoft}`}}>
            <span style={{width:8,height:8,borderRadius:999,background:col,boxShadow:`0 0 8px ${col}`,flexShrink:0}}/>
            <span style={{fontSize:10.5,fontWeight:700,color:COLORS.ink,fontFamily:MONO_FONT,letterSpacing:0.5,textTransform:"uppercase",flex:1,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"}}>{s.name}</span>
            <span style={{fontFamily:NUM_FONT,fontSize:11,fontWeight:700,color:COLORS.inkMuted,background:"var(--th-row-soft-2)",padding:"1px 7px",borderRadius:999}}>{items.length}</span>
          </div>
          {!items.length&&<div style={{fontSize:10.5,color:COLORS.inkSubtle,textAlign:"center",padding:"16px 6px",border:`1px dashed ${COLORS.hairline}`,borderRadius:8,lineHeight:1.4}}>{canEdit?"ลากการ์ดมาที่นี่":"ว่าง"}</div>}
          {items.map(t=>{const pr=PRIORITY[t.priority]||{},dragging=dragId===t.id,editing=editId===t.id;
            let clT=0,clD=0; try{const c=JSON.parse(t.checklist||"[]");clT=c.length;clD=c.filter(x=>x.done).length;}catch(_){}
            const saveTitle=()=>{ const v=editVal.trim(); if(v&&v!==t.title) patchTask(t.id,{title:v}); setEditId(null); };
            return <div key={t.id}
            draggable={canEdit&&!busy&&!editing}
            onDragStart={canEdit&&!editing?(e=>{setDragId(t.id);try{e.dataTransfer.effectAllowed="move";}catch(_){}}):undefined}
            onDragEnd={canEdit?(()=>{setDragId(null);setOverId(null);}):undefined}
            onClick={()=>{ if(!editing) onOpenTask&&onOpenTask(t.id); }}
            style={{background:COLORS.card,border:`1px solid ${COLORS.hairline}`,borderRadius:10,padding:"9px 11px",cursor:canEdit?"grab":"pointer",opacity:dragging?0.4:1}}>
            {editing
              ? <input autoFocus value={editVal} onClick={e=>e.stopPropagation()} onChange={e=>setEditVal(e.target.value)}
                  onKeyDown={e=>{ if(e.key==="Enter"){e.preventDefault();saveTitle();} else if(e.key==="Escape"){setEditId(null);} }} onBlur={saveTitle}
                  style={{width:"100%",fontSize:12.5,fontWeight:600,padding:"3px 5px",borderRadius:6,border:`1px solid ${COLORS.accentEdge}`,background:COLORS.bgElev,color:COLORS.ink,marginBottom:7,boxSizing:"border-box"}}/>
              : <div onDoubleClick={canEdit?(e=>{e.stopPropagation();setEditId(t.id);setEditVal(t.title);}):undefined} title={canEdit?"ดับเบิลคลิกแก้ชื่อ":""}
                  style={{fontSize:12.5,color:COLORS.ink,fontWeight:600,lineHeight:1.35,marginBottom:7}}>{t.title}</div>}
            <div style={{display:"flex",alignItems:"center",gap:6}}>
              <span onClick={canEdit?(e=>{e.stopPropagation();cyclePri(t);}):undefined} title={canEdit?"คลิกเปลี่ยนความสำคัญ":""}
                style={{fontSize:9,fontWeight:700,color:pr.color||COLORS.inkMuted,fontFamily:MONO_FONT,cursor:canEdit?"pointer":"default"}}>● {t.priority}</span>
              {clT>0 && <span style={{fontSize:9,fontWeight:700,fontFamily:MONO_FONT,color:clD===clT?STATUS.done.fg:COLORS.inkSubtle}}>☑{clD}/{clT}</span>}
              {dateId===t.id
                ? <input type="date" autoFocus value={t.due||""} onClick={e=>e.stopPropagation()} onChange={e=>{ if(e.target.value) patchTask(t.id,{due:e.target.value}); setDateId(null); }} onBlur={()=>setDateId(null)}
                    style={{fontSize:9,padding:"1px 3px",borderRadius:5,border:`1px solid ${COLORS.accentEdge}`,background:COLORS.bgElev,color:COLORS.ink}}/>
                : <span onClick={canEdit?(e=>{e.stopPropagation();setDateId(t.id);}):undefined} title={canEdit?"คลิกตั้งวันที่":""}
                    style={{fontSize:9.5,color:COLORS.inkSubtle,cursor:canEdit?"pointer":"default"}}>{t.due?("📅 "+(""+t.due).slice(5)):(canEdit?"＋📅":"")}</span>}
              <div style={{flex:1}}/>
              {t.owner_name&&<span style={{fontSize:10.5,color:COLORS.inkMuted,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis",maxWidth:64}}>{t.owner_name}</span>}
            </div>
          </div>;})}
          {canEdit && (addCol===s.id
            ? <ColQuickAdd stageId={s.id} pipelineKey={pipelineKey} brand={brand!=="all"?brand:""} user={user} users={users} canSeeAll={canSeeAll} onAdded={loadTasks} onClose={()=>setAddCol(null)}/>
            : <button onClick={()=>setAddCol(s.id)} style={{padding:"7px",borderRadius:8,border:`1px dashed ${COLORS.hairline}`,background:"transparent",color:COLORS.inkMuted,fontSize:11.5,fontWeight:600,cursor:"pointer",width:"100%"}}>＋ เพิ่มงาน</button>)}
        </div>;})}
    </div>
    </div>
  </div>;
}

// Sidebar (desktop · admin/head) — Workspace nav + Pipelines + user card (match design)
function Sidebar({user,view,setView,filter,setFilter,pipelines,counts,onLogout,canSeeAll,mine,setMine,reviewRoles,reviewMode,setReviewMode,reviewCount,todayMode,setTodayMode,todayCount,reworkMode,setReworkMode,reworkCount,mineNew,onMineSeen,setOwnerFilter,usersList,onImpersonate,archiveMode,setArchiveMode,trashMode,setTrashMode,canDelete,search,setSearch,onNav}){
  const clearRev=()=>{setReviewMode&&setReviewMode(false);setTodayMode&&setTodayMode(false);setReworkMode&&setReworkMode(false);setOwnerFilter&&setOwnerFilter(null);setArchiveMode&&setArchiveMode(false);setTrashMode&&setTrashMode(false);};
  const navBadge=(n,c)=>n>0?<span style={{fontFamily:NUM_FONT,fontSize:10.5,fontWeight:700,color:c||COLORS.accentBright,background:(c||COLORS.accentBright)+"22",border:`1px solid ${(c||COLORS.accentBright)}55`,borderRadius:999,padding:"0 6px",minWidth:18,textAlign:"center"}}>{n}</span>:null;
  const nav=(active,emoji,label,onClick,trailing)=>(
    <div onClick={()=>{onClick&&onClick();onNav&&onNav();}} style={{display:"flex",alignItems:"center",gap:10,padding:"8px 12px",borderRadius:8,margin:"0 10px",cursor:"pointer",fontSize:13,fontWeight:active?700:500,
      background:active?COLORS.accentSoft:"transparent",color:active?COLORS.accentBright:COLORS.ink,border:`1px solid ${active?COLORS.accentEdge:"transparent"}`}}>
      <span style={{width:18,textAlign:"center"}}>{emoji}</span><span style={{flex:1,whiteSpace:"nowrap"}}>{label}</span>{trailing||null}
    </div>
  );
  const sec=(label,children)=>(<div style={{marginBottom:14}}>
    <div style={{padding:"8px 20px 6px",fontSize:9.5,fontWeight:700,color:COLORS.inkSubtle,letterSpacing:1.5,fontFamily:MONO_FONT,textTransform:"uppercase"}}>{label}</div>
    <div style={{display:"flex",flexDirection:"column",gap:2}}>{children}</div>
  </div>);
  return <div style={{width:230,flexShrink:0,background:"var(--th-sidebar)",borderRight:`1px solid ${COLORS.hairline}`,display:"flex",flexDirection:"column",position:"sticky",top:0,height:"100vh",overflowY:"auto"}}>
    <div style={{display:"flex",alignItems:"center",gap:10,padding:"18px 20px 22px"}}>
      <div style={{width:26,height:26,borderRadius:8,background:`linear-gradient(135deg,${COLORS.accentBright},${COLORS.accent})`,boxShadow:`0 0 14px ${COLORS.accentGlow}`,flexShrink:0}}/>
      <div><div style={{fontSize:14,fontWeight:700,color:COLORS.inkBright,lineHeight:1,letterSpacing:1}}>NEWVERSE</div>
        <div style={{fontSize:8.5,color:COLORS.inkSubtle,fontFamily:MONO_FONT,letterSpacing:1.5,marginTop:3}}>TEAM TASK HUB</div></div>
    </div>
    {/* #5 · ค้นหาถาวร (Notion-style) */}
    <div style={{padding:"0 14px 12px"}}>
      <input value={search||""} onChange={e=>setSearch&&setSearch(e.target.value)} onFocus={()=>setView&&setView("board")}
        placeholder="🔍 ค้นหางาน…" style={{width:"100%",padding:"8px 11px",borderRadius:9,background:COLORS.card,border:`1px solid ${search?COLORS.accentEdge:COLORS.hairline}`,color:COLORS.ink,fontSize:12.5,boxSizing:"border-box"}}/>
    </div>
    {sec("มุมมอง",<>
      {canSeeAll && nav(view==="overview","📊","Dashboard",()=>{clearRev();setView("overview");})}
      {nav(view==="board"&&!!todayMode,"📅",`วันนี้${todayCount?` (${todayCount})`:""}`,()=>{clearRev();setView("board");setFilter("all");setMine&&setMine(false);setTodayMode&&setTodayMode(true);})}
      {nav(view==="board"&&filter==="all"&&!mine&&!reviewMode&&!todayMode&&!reworkMode&&!archiveMode&&!trashMode,"📋","งานทั้งหมด",()=>{clearRev();setView("board");setFilter("all");setMine&&setMine(false);})}
      {nav(view==="calendar","📆","ปฏิทิน",()=>{clearRev();setView("calendar");setFilter("all");setMine&&setMine(false);})}
      {nav(view==="week7","🗓","7 วันข้างหน้า",()=>{clearRev();setView("week7");setFilter("all");setMine&&setMine(false);})}
      {nav(view==="availability","📅","ตารางทีม ลา/WFH",()=>{clearRev();setView("availability");})}
      {nav(!!mine&&!reviewMode&&!todayMode&&!reworkMode,"🙋","งานของฉัน",()=>{clearRev();setView("board");setMine&&setMine(true);onMineSeen&&onMineSeen();},navBadge(mineNew,"#6EE5A0"))}
    </>)}
    {sec("ต้องจัดการ",<>
      {nav(!!reworkMode,"↩️","ตีกลับ ต้องแก้",()=>{clearRev();setView("board");setFilter("all");setMine&&setMine(false);setReworkMode&&setReworkMode(true);},navBadge(reworkCount,"var(--th-st-red-fg)"))}
      {reviewRoles && nav(!!reviewMode,"🛡","รอรีวิว",()=>{clearRev();setView("board");setFilter("all");setMine&&setMine(false);setReviewMode&&setReviewMode(true);},navBadge(reviewCount))}
      {nav(!!archiveMode,"🗄","คลังงาน",()=>{clearRev();setView("board");setFilter("all");setMine&&setMine(false);setArchiveMode&&setArchiveMode(true);})}
      {canDelete && nav(!!trashMode,"🗑","ถังขยะ",()=>{clearRev();setView("board");setFilter("all");setMine&&setMine(false);setTrashMode&&setTrashMode(true);})}
    </>)}
    {/* frontend-split: ตัดลิงก์ CRM (/crm) และ KB (/kb) ออก — โมดูลลับ อยู่คนละ frontend/route */}
    {(["admin","exec","superadmin"].includes(user.role)) && sec("ทีม",<>
      {["admin","exec","superadmin"].includes(user.role) && nav(view==="activity","🟢","ใครออนไลน์ · Activity",()=>{clearRev();setView("activity");})}
    </>)}
    {sec(canSeeAll?"Pipelines":"สายงานของฉัน",(pipelines||[]).map(p=>{const tk=pipeTok(p.key),active=view==="board"&&filter===p.key;
      return <div key={p.key} onClick={()=>{clearRev();setView("board");setFilter(p.key);onNav&&onNav();}} style={{display:"flex",alignItems:"center",gap:10,padding:"7px 12px",borderRadius:8,margin:"0 10px",cursor:"pointer",fontSize:12.5,fontWeight:active?700:500,color:active?COLORS.inkBright:COLORS.ink,background:active?"var(--th-hover-soft)":"transparent",border:`1px solid ${active?COLORS.hairline:"transparent"}`}}>
        <span style={{width:9,height:9,borderRadius:999,background:tk.color,boxShadow:`0 0 8px ${tk.color}`,flexShrink:0}}/>
        <span style={{flex:1,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"}}>{p.name||tk.label}</span>
        <span style={{fontFamily:NUM_FONT,fontSize:11,fontWeight:700,color:COLORS.inkMuted}}>{counts[p.key]||0}</span>
      </div>;}))}
    {["superadmin","admin"].includes(user.role) && (usersList||[]).length>0 && sec("👁 ดูในมุมมองของ",
      <div style={{padding:"0 10px"}}>
        <select value="" onChange={e=>{ if(e.target.value) onImpersonate&&onImpersonate(+e.target.value); }} style={{width:"100%",padding:"8px 10px",borderRadius:9,background:COLORS.card,border:`1px solid ${COLORS.accentEdge}`,color:COLORS.accentBright,fontSize:12.5,fontWeight:600,cursor:"pointer"}}>
          <option value="">เลือกคน…</option>
          {usersList.filter(u=>u.id!==user.id && u.active!==0 && (user.role==="superadmin" || u.role!=="superadmin")).map(u=><option key={u.id} value={u.id}>{u.name} · {u.role}</option>)}
        </select>
        <div style={{fontSize:10,color:COLORS.inkSubtle,margin:"5px 4px 0",lineHeight:1.4}}>เห็น+ใช้งานเสมือนเป็นเขา</div>
      </div>)}
    <div style={{flex:1}}/>
    <div style={{margin:10,padding:"10px 12px",background:"var(--th-hover-soft)",borderRadius:10,border:`1px solid ${COLORS.hairline}`,display:"flex",alignItems:"center",gap:10}}>
      <div style={{width:32,height:32,borderRadius:999,background:COLORS.accentSoft,display:"flex",alignItems:"center",justifyContent:"center",color:COLORS.accentBright,fontWeight:700,flexShrink:0}}>{(user.name||"?")[0]}</div>
      <div style={{flex:1,minWidth:0}}>
        <div style={{fontSize:12.5,fontWeight:700,color:COLORS.inkBright,lineHeight:1.1,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"}}>{user.name}</div>
        <div style={{fontSize:9.5,color:COLORS.accentBright,fontFamily:MONO_FONT,letterSpacing:0.6,marginTop:2}}>{(user.role||"").toUpperCase()}</div></div>
      <span onClick={onLogout} title="ออกจากระบบ" style={{cursor:"pointer",color:COLORS.inkMuted,fontSize:15}}>⏻</span>
    </div>
  </div>;
}

// จอ ≥1024px (MacBook/desktop) → master-detail · เล็กกว่า → mobile modal
function useIsDesktop(){
  const [d,setD]=useState(typeof window!=="undefined" && window.matchMedia("(min-width:1024px)").matches);
  useEffect(()=>{ const m=window.matchMedia("(min-width:1024px)"); const h=e=>setD(e.matches);
    m.addEventListener?.("change",h); return ()=>m.removeEventListener?.("change",h); },[]);
  return d;
}
// ปฏิทินรายเดือน — งานเรียงตาม due · คลิกการ์ดเปิดงาน
function CalendarView({tasks,onOpenTask,onAddDay,onReschedule}){
  const today=new Date(); today.setHours(0,0,0,0);
  const [ym,setYm]=useState({y:today.getFullYear(),m:today.getMonth()});
  const [dragId,setDragId]=useState(null); const [overK,setOverK]=useState(null);
  const fmt=(y,m,d)=>`${y}-${String(m+1).padStart(2,"0")}-${String(d).padStart(2,"0")}`;
  const startDow=new Date(ym.y,ym.m,1).getDay();
  const daysInMonth=new Date(ym.y,ym.m+1,0).getDate();
  const byDue={}; (tasks||[]).forEach(t=>{ if(t.due){ const k=(""+t.due).slice(0,10); (byDue[k]=byDue[k]||[]).push(t); } });
  const cells=[]; for(let i=0;i<startDow;i++)cells.push(null); for(let d=1;d<=daysInMonth;d++)cells.push(d); while(cells.length%7)cells.push(null);
  const DOW=["อา","จ","อ","พ","พฤ","ศ","ส"];
  const todayStr=fmt(today.getFullYear(),today.getMonth(),today.getDate());
  const nb={width:32,height:32,borderRadius:8,background:COLORS.card,border:`1px solid ${COLORS.hairline}`,color:COLORS.ink,fontSize:16,cursor:"pointer"};
  const isDesktop=useIsDesktop();
  const goPrev=()=>setYm(s=>{const m=s.m-1;return m<0?{y:s.y-1,m:11}:{y:s.y,m};});
  const goNext=()=>setYm(s=>{const m=s.m+1;return m>11?{y:s.y+1,m:0}:{y:s.y,m};});
  const goToday=()=>setYm({y:today.getFullYear(),m:today.getMonth()});
  // มือถือ: month grid 7 คอลัมน์อ่านไม่ออก → Agenda รายวัน (คุณเอก 23 มิ.ย.)
  if(!isDesktop){
    const aRow=(t)=>{const p=pipeTok(t.pipeline_key);const done=DONE_STATES.includes(t.status);return <div key={t.id} onClick={()=>onOpenTask&&onOpenTask(t)} style={{display:"flex",alignItems:"center",gap:9,padding:"10px 12px",borderRadius:10,background:COLORS.card,border:`1px solid ${COLORS.hairline}`,cursor:"pointer",marginBottom:7}}><span style={{width:8,height:8,borderRadius:999,background:p.color,flexShrink:0}}/><span style={{flex:1,fontSize:14,fontWeight:600,color:done?COLORS.inkSubtle:COLORS.ink,textDecoration:done?"line-through":"none",lineHeight:1.35}}>{t.recurrence?"🔁 ":""}{t.title}</span>{t.priority&&<span style={{fontFamily:MONO_FONT,fontSize:10.5,fontWeight:700,color:"var(--th-st-orange-fg)"}}>{t.priority}</span>}</div>;};
    const monthDays=[]; for(let dd=1;dd<=daysInMonth;dd++){const k=fmt(ym.y,ym.m,dd);if(byDue[k]&&byDue[k].length)monthDays.push({d:dd,k,items:byDue[k]});}
    const overdue=(tasks||[]).filter(t=>t.due&&(""+t.due).slice(0,10)<todayStr&&!DONE_STATES.includes(t.status)).sort((a,b)=>(""+a.due).localeCompare(""+b.due));
    return <div style={{maxWidth:680,margin:"0 auto",padding:"10px 14px 90px"}}>
      <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:14}}>
        <div style={{fontSize:18,fontWeight:700,color:COLORS.inkBright}}>{THMON[ym.m]} {ym.y+543}</div><div style={{flex:1}}/>
        <button onClick={goPrev} style={nb}>‹</button>
        <button onClick={goToday} style={{...nb,width:"auto",padding:"0 12px",fontSize:12.5,fontWeight:700}}>วันนี้</button>
        <button onClick={goNext} style={nb}>›</button>
      </div>
      {overdue.length>0&&<div style={{marginBottom:18}}><div style={{fontSize:12,fontWeight:700,color:"var(--th-st-red-fg)",fontFamily:MONO_FONT,letterSpacing:0.5,marginBottom:8}}>🔴 เลยกำหนด ({overdue.length})</div>{overdue.map(aRow)}</div>}
      {monthDays.length===0&&<div style={{textAlign:"center",color:COLORS.inkSubtle,fontSize:13,padding:"40px 0"}}>ไม่มีงานในเดือนนี้</div>}
      {monthDays.map(({d,k,items})=>{const dt=new Date(ym.y,ym.m,d);const isToday=k===todayStr;return <div key={k} style={{marginBottom:16}}><div style={{display:"flex",alignItems:"center",gap:8,marginBottom:8}}><div style={{fontSize:13.5,fontWeight:700,color:isToday?COLORS.accentBright:COLORS.inkMuted}}>{DOW[dt.getDay()]} {d} {THMON[ym.m]}{isToday?" · วันนี้":""}</div><div style={{flex:1,height:1,background:COLORS.hairline}}/>{onAddDay&&<button onClick={()=>onAddDay(k)} title="เพิ่มงาน" style={{width:24,height:24,borderRadius:7,border:`1px solid ${COLORS.hairline}`,background:"transparent",color:COLORS.accentBright,fontSize:16,lineHeight:1,cursor:"pointer",padding:0}}>＋</button>}</div>{items.map(aRow)}</div>;})}
    </div>;
  }
  return <div style={{maxWidth:1760,margin:"0 auto",padding:"10px 18px 30px"}}>
    <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:14}}>
      <div style={{fontSize:18,fontWeight:700,color:COLORS.inkBright}}>{THMON[ym.m]} {ym.y+543}</div>
      <div style={{flex:1}}/>
      <button onClick={()=>setYm(s=>{const m=s.m-1;return m<0?{y:s.y-1,m:11}:{y:s.y,m};})} style={nb}>‹</button>
      <button onClick={()=>setYm({y:today.getFullYear(),m:today.getMonth()})} style={{...nb,width:"auto",padding:"0 12px",fontSize:12.5,fontWeight:700}}>วันนี้</button>
      <button onClick={()=>setYm(s=>{const m=s.m+1;return m>11?{y:s.y+1,m:0}:{y:s.y,m};})} style={nb}>›</button>
    </div>
    <div style={{display:"grid",gridTemplateColumns:"repeat(7,1fr)",gap:6}}>
      {DOW.map(d=><div key={d} style={{fontSize:11,fontWeight:700,color:COLORS.inkSubtle,fontFamily:MONO_FONT,textAlign:"center",padding:"4px 0"}}>{d}</div>)}
      {cells.map((d,i)=>{ if(d==null)return <div key={i}/>; const k=fmt(ym.y,ym.m,d); const isToday=k===todayStr; const items=byDue[k]||[]; const hot=overK===k&&dragId;
        return <div key={i}
          onDragOver={onReschedule?(e=>{e.preventDefault();setOverK(k);}):undefined}
          onDragLeave={onReschedule?(()=>setOverK(o=>o===k?null:o)):undefined}
          onDrop={onReschedule?(()=>{const id=dragId;setOverK(null);setDragId(null);if(id)onReschedule(id,k);}):undefined}
          style={{minHeight:94,background:hot?"var(--th-hover-accent-3)":(isToday?COLORS.accentSoft:"var(--th-hover-2)"),border:`1px solid ${hot?COLORS.accentBright:(isToday?COLORS.accentEdge:COLORS.hairline)}`,borderRadius:10,padding:6,display:"flex",flexDirection:"column",gap:3,overflow:"hidden",transition:"background .12s"}}>
          <div style={{display:"flex",alignItems:"center",gap:2}}>
            <div style={{flex:1,fontSize:11,fontWeight:700,color:isToday?COLORS.accentBright:COLORS.inkMuted,fontFamily:NUM_FONT}}>{d}</div>
            {onAddDay&&<button onClick={e=>{e.stopPropagation();onAddDay(k);}} title="เพิ่มงานวันนี้" style={{width:18,height:18,borderRadius:6,border:"none",background:"transparent",color:COLORS.inkSubtle,fontSize:15,lineHeight:1,cursor:"pointer",padding:0,opacity:0.7}}>＋</button>}
          </div>
          {items.slice(0,4).map(t=>{const p=pipeTok(t.pipeline_key);const done=DONE_STATES.includes(t.status);return <div key={t.id} onClick={()=>onOpenTask&&onOpenTask(t)} title={t.title}
            draggable={!!onReschedule}
            onDragStart={onReschedule?(e=>{setDragId(t.id);try{e.dataTransfer.effectAllowed="move";}catch(_){}}):undefined}
            onDragEnd={onReschedule?(()=>{setDragId(null);setOverK(null);}):undefined}
            style={{fontSize:10.5,padding:"2px 5px",borderRadius:5,background:p.soft,color:done?COLORS.inkSubtle:p.color,cursor:onReschedule?"grab":"pointer",whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis",textDecoration:done?"line-through":"none",fontWeight:600,opacity:dragId===t.id?0.4:1}}>{t.recurrence?"🔁 ":""}{t.title}</div>;})}
          {items.length>4&&<div style={{fontSize:9.5,color:COLORS.inkSubtle,paddingLeft:3}}>+{items.length-4} เพิ่มเติม</div>}
        </div>;})}
    </div>
  </div>;
}
// #5 · 7 วันข้างหน้า — 7 คอลัมน์ (วันนี้ + อีก 6 วัน) · งานตาม due แต่ละวัน
function Week7View({tasks,onOpenTask,canEdit,onAddDay,onReschedule}){
  const [dragId,setDragId]=React.useState(null); const [overIso,setOverIso]=React.useState(null);
  const base=bkkToday().split('-').map(Number); const todayIso=bkkToday(); const DOW=["อา","จ","อ","พ","พฤ","ศ","ส"];
  const days=[]; for(let i=0;i<7;i++){ const d=new Date(Date.UTC(base[0],base[1]-1,base[2])); d.setUTCDate(d.getUTCDate()+i);
    days.push({iso:d.toISOString().slice(0,10),dow:DOW[d.getUTCDay()],dnum:d.getUTCDate(),mon:THMON[d.getUTCMonth()]}); }
  const byDue={}; (tasks||[]).forEach(t=>{ if(t.due){ const k=(""+t.due).slice(0,10); (byDue[k]=byDue[k]||[]).push(t); } });
  const PR={P0:0,P1:1,P2:2,P3:3};
  const isDesktop=useIsDesktop();   // มือถือ: เรียงแนวตั้ง · เดสก์ท็อป: แนวนอนยืดเต็ม
  return <div style={{padding:"10px 18px 30px"}}>
    <div style={{fontSize:11,color:COLORS.inkSubtle,marginBottom:10}}>🗓 7 วันข้างหน้า · งานตามวันกำหนด</div>
    <div style={{display:"flex",flexDirection:isDesktop?"row":"column",gap:10,alignItems:isDesktop?"flex-start":"stretch",overflowX:isDesktop?"auto":"visible",paddingBottom:8}}>
      {days.map(day=>{ const isToday=day.iso===todayIso; const items=(byDue[day.iso]||[]).slice().sort((a,b)=>(PR[a.priority]??9)-(PR[b.priority]??9)); const hot=overIso===day.iso&&dragId;
        return <div key={day.iso}
          onDragOver={onReschedule?(e=>{e.preventDefault();setOverIso(day.iso);}):undefined}
          onDragLeave={onReschedule?(()=>setOverIso(o=>o===day.iso?null:o)):undefined}
          onDrop={onReschedule?(()=>{const id=dragId;setOverIso(null);setDragId(null);if(id)onReschedule(id,day.iso);}):undefined}
          style={{flex:isDesktop?"1 1 180px":"1 1 auto",minWidth:isDesktop?160:0,maxWidth:isDesktop?300:"none",width:isDesktop?"auto":"100%",background:hot?"var(--th-hover-accent-2)":(isToday?COLORS.accentSoft:"var(--th-hover-2)"),border:`1px solid ${hot?COLORS.accentBright:(isToday?COLORS.accentEdge:COLORS.hairline)}`,borderRadius:12,padding:9,display:"flex",flexDirection:"column",gap:7,transition:"background .12s"}}>
          <div style={{display:"flex",alignItems:"center",gap:6,paddingBottom:7,borderBottom:`1px solid ${COLORS.hairlineSoft}`}}>
            <span style={{fontSize:11,fontWeight:700,color:isToday?COLORS.accentBright:COLORS.ink,fontFamily:MONO_FONT}}>{isToday?"วันนี้":day.dow} {day.dnum} {day.mon}</span>
            <div style={{flex:1}}/>
            <span style={{fontFamily:NUM_FONT,fontSize:11,fontWeight:700,color:COLORS.inkMuted}}>{items.length}</span>
          </div>
          {items.length===0&&<div style={{fontSize:10.5,color:COLORS.inkSubtle,textAlign:"center",padding:"10px 4px"}}>ว่าง</div>}
          {items.map(t=>{const p=pipeTok(t.pipeline_key),pr=PRIORITY[t.priority]||{},done=DONE_STATES.includes(t.status);
            return <div key={t.id} onClick={()=>onOpenTask&&onOpenTask(t.id)}
              draggable={!!onReschedule}
              onDragStart={onReschedule?(e=>{setDragId(t.id);try{e.dataTransfer.effectAllowed="move";}catch(_){}}):undefined}
              onDragEnd={onReschedule?(()=>{setDragId(null);setOverIso(null);}):undefined}
              style={{background:COLORS.card,border:`1px solid ${COLORS.hairline}`,borderRadius:9,padding:"7px 9px",cursor:onReschedule?"grab":"pointer",opacity:done?0.55:(dragId===t.id?0.4:1)}}>
              <div style={{fontSize:12,fontWeight:600,color:COLORS.ink,lineHeight:1.3,marginBottom:5,textDecoration:done?"line-through":"none"}}>{t.recurrence?"🔁 ":""}{t.title}</div>
              <div style={{display:"flex",alignItems:"center",gap:6}}>
                <span style={{fontSize:8.5,fontWeight:700,color:pr.color||COLORS.inkMuted,fontFamily:MONO_FONT}}>● {t.priority}</span>
                <span style={{width:7,height:7,borderRadius:999,background:p.color,flexShrink:0}}/>
                <div style={{flex:1}}/>
                {t.owner_name&&<span style={{fontSize:9.5,color:COLORS.inkMuted,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis",maxWidth:78}}>{t.owner_name}</span>}
              </div>
            </div>;})}
          {canEdit&&onAddDay&&<button onClick={()=>onAddDay(day.iso)} style={{padding:"6px",borderRadius:8,border:`1px dashed ${COLORS.hairline}`,background:"transparent",color:COLORS.inkSubtle,fontSize:11,fontWeight:600,cursor:"pointer"}}>＋ เพิ่ม</button>}
        </div>;})}
    </div>
  </div>;
}
// ถังขยะ — งานที่ลบ (soft) · กู้คืน / ลบถาวร · auto-purge 30 วัน
function TrashView({list, onRestore, onPurge}){
  if(list===null) return <div style={{color:COLORS.inkMuted,textAlign:"center",padding:40}}>กำลังโหลดถังขยะ...</div>;
  return <div style={{maxWidth:760,margin:"0 auto",padding:"6px 18px 30px"}}>
    <div style={{fontSize:12.5,color:COLORS.inkMuted,marginBottom:14,display:"flex",alignItems:"center",gap:7}}><span>🗑</span> ถังขยะ · {list.length} งาน · ลบถาวรอัตโนมัติหลัง 30 วัน</div>
    {list.length===0 && <div style={{color:COLORS.inkSubtle,textAlign:"center",padding:40}}>ถังขยะว่าง</div>}
    {list.map(t=>{ const p=pipeTok(t.pipeline_key);
      return <div key={t.id} style={{display:"flex",alignItems:"center",gap:11,background:COLORS.card,border:`1px solid ${COLORS.hairline}`,borderRadius:12,padding:"11px 14px",marginBottom:9}}>
        <div style={{flex:1,minWidth:0}}>
          <div style={{fontSize:14,fontWeight:600,color:COLORS.inkMuted,textDecoration:"line-through",whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"}}>{t.title}</div>
          <div style={{fontSize:10.5,color:COLORS.inkSubtle,fontFamily:MONO_FONT,marginTop:3}}>{p.label} · ลบเมื่อ {(t.deleted_at||"").slice(5,16).replace("T"," ")} · เหลือ {t.days_left} วัน</div>
        </div>
        <button onClick={()=>onRestore(t.id)} style={{padding:"6px 13px",borderRadius:999,background:COLORS.accentSoft,color:COLORS.accentBright,border:`1px solid ${COLORS.accentEdge}`,fontWeight:700,fontSize:12.5,cursor:"pointer",whiteSpace:"nowrap"}}>↩ กู้คืน</button>
        <button onClick={()=>onPurge(t.id)} style={{padding:"6px 11px",borderRadius:999,background:"transparent",color:"var(--th-st-red-fg)",border:`1px solid color-mix(in srgb, var(--th-st-red-fg) 40%, transparent)`,fontWeight:600,fontSize:12.5,cursor:"pointer",whiteSpace:"nowrap"}}>ลบถาวร</button>
      </div>; })}
  </div>;
}
// ---- theme toggle (🌙/☀️) · own state so it re-renders its icon · CSS vars handle the rest ----
function ThemeToggle({compact}){
  const [theme,setTh]=useState(getTheme());
  const flip=()=>{ const v=toggleTheme(); setTh(v); };
  const light=theme==="light";
  return <button onClick={flip} title={light?"สลับเป็นธีมมืด":"สลับเป็นธีมสว่าง"}
    aria-label="สลับธีม"
    style={{background:COLORS.card,color:COLORS.inkMuted,border:`1px solid ${COLORS.hairline}`,borderRadius:10,
      ...(compact?{width:38,height:38,fontSize:17,display:"flex",alignItems:"center",justifyContent:"center",lineHeight:1}
                 :{padding:"8px 12px",fontSize:14,lineHeight:1})}}>
    {light?"🌙":"☀️"}</button>;
}
function Board({user, onLogout}){
  const [tasks,setTasks]=useState([]); const [stats,setStats]=useState(null);
  const [pipelines,setPipelines]=useState([]); const [users,setUsers]=useState([]);
  const [filter,setFilter]=useState("all"); const [mine,setMine]=useState(false); const [view,setView]=useState(()=>{try{return new URLSearchParams(location.search).get("view")==="availability"?"availability":"board";}catch(_){return "board";}});
  const [search,setSearch]=useState(""); const [reviewMode,setReviewMode]=useState(false);
  const [todayMode,setTodayMode]=useState(false); const [sortBy,setSortBy]=useState("smart");
  const [reworkMode,setReworkMode]=useState(false); const [mineNew,setMineNew]=useState(0);
  const [archiveMode,setArchiveMode]=useState(false); const [trashMode,setTrashMode]=useState(false); const [trashList,setTrashList]=useState(null);
  const [sel,setSel]=useState(null); const [creating,setCreating]=useState(false); const [loading,setLoading]=useState(true); const [pwOpen,setPwOpen]=useState(false);
  const [createDue,setCreateDue]=useState("");   // ปฏิทินคลิกวันที่ → prefill due
  const [drawerOpen,setDrawerOpen]=useState(false);   // #1 mobile sidebar drawer
  const [cursor,setCursor]=useState(-1); const searchRef=React.useRef(null);
  const shownRef=React.useRef([]); const cursorRef=React.useRef(-1); const kbActions=React.useRef({});
  const [selMode,setSelMode]=useState(false); const [picked,setPicked]=useState(()=>new Set());
  const [ownerFilter,setOwnerFilter]=useState(null); const [tagFilter,setTagFilter]=useState("");
  const canSeeAll=["head_content","pm","admin","service","exec","superadmin"].includes(user.role);
  const canEdit=user.role!=="pm";
  async function toggleDone(t){ const done=DONE_STATES.includes(t.status); const next=done?"progress":"done"; const prev=t.status;
    setTasks(ts=>ts.map(x=>x.id===t.id?{...x,status:next}:x));              // optimistic
    setSel(s=>(s&&s.id===t.id)?{...s,status:next}:s);
    try{ await api("PATCH","/api/tasks/"+t.id,{status:next});
      if(!done) toast.ok(`ทำเสร็จ · ${(""+t.title).slice(0,30)}`,{actionLabel:"เลิกทำ",action:()=>{
        setTasks(ts=>ts.map(x=>x.id===t.id?{...x,status:prev}:x)); setSel(s=>(s&&s.id===t.id)?{...s,status:prev}:s);
        api("PATCH","/api/tasks/"+t.id,{status:prev}).then(load).catch(()=>{}); }});
      load();
    }catch(e){ setTasks(ts=>ts.map(x=>x.id===t.id?{...x,status:prev}:x)); setSel(s=>(s&&s.id===t.id)?{...s,status:prev}:s); toast.error(e.message); } }
  async function quickAdd(text){ const {title,priority,due,recurrence}=parseQuick(text);
    let pk = (filter!=="all" && filter) || user.pipeline_key || (visiblePipes[0]&&visiblePipes[0].key);
    if(!title||!pk) return;
    try{ await api("POST","/api/tasks",{title,pipeline_key:pk,priority:priority||"P2",due,recurrence:recurrence||null,status:"idea"}); load(); }catch(e){ toast.error(e.message); } }
  async function impersonate(uid){
    const tgt=(users||[]).find(u=>u.id===uid)||{};
    if(!await confirmAsk({title:"สวมบทเป็นคนนี้?",message:`คุณจะเข้าสู่มุมมองของ ${tgt.name||"ผู้ใช้ #"+uid}${tgt.role?` (${tgt.role})`:""}\n\nทุกการแก้/ลบ/ย้าย จะทำในนามนี้ และถูกบันทึก audit`,confirmLabel:"สวมบท"})) return;
    try{ const r=await api("POST","/api/impersonate",{user_id:uid});
      localStorage.setItem("th_real_token", token()); localStorage.setItem("th_real_user", JSON.stringify(user));
      setToken(r.token); setUser(r.user); location.reload();
    }catch(e){ toast.error(e.message); } }
  const loadTrash=useCallback(()=>{ api("GET","/api/trash").then(setTrashList).catch(()=>setTrashList([])); },[]);
  useEffect(()=>{ if(trashMode) loadTrash(); },[trashMode,loadTrash]);
  async function restoreTask(id){ try{ await api("POST","/api/tasks/"+id+"/restore"); toast.ok("กู้คืนงานแล้ว"); loadTrash(); load(); }catch(e){ toast.error(e.message); } }
  async function purgeTask(id){
    if(!await gate({title:"ลบถาวร?",body:"งานจะถูกลบอย่างถาวร · กู้คืนไม่ได้อีก",danger:true,requireText:"ลบถาวร",confirmLabel:"ลบถาวร"})) return;
    try{ await api("DELETE","/api/trash/"+id); toast.ok("ลบถาวรแล้ว"); loadTrash(); }catch(e){ toast.error(e.message); } }
  const togglePick=(t)=>setPicked(s=>{const n=new Set(s); n.has(t.id)?n.delete(t.id):n.add(t.id); return n;});
  const exitSel=()=>{ setSelMode(false); setPicked(new Set()); };
  async function bulkPatch(body,label){ const ids=[...picked]; if(!ids.length){ exitSel(); return; }
    if(isImp() && !await gate({title:`${label} ${ids.length} งาน?`,body:`ดำเนินการกับ ${ids.length} งานพร้อมกัน`})) return;   // สวมบท → ยืนยันก่อน bulk
    let ok=0,fail=0; for(const id of ids){ try{ await api("PATCH","/api/tasks/"+id,body); ok++; }catch(e){ fail++; } }
    exitSel(); await load(); (fail?toast.error:toast.ok)(`${label} ${ok} งาน${fail?` · พลาด ${fail}`:""}`); }
  function bulkReschedule(n){ const d=new Date(); d.setHours(0,0,0,0); d.setDate(d.getDate()+n);
    bulkPatch({due:`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`},"เลื่อนกำหนด"); }
  const isDesktop=useIsDesktop();

  const load=useCallback(async(silent)=>{
    if(!silent) setLoading(true);
    const q=new URLSearchParams();
    if(filter!=="all") q.set("pipeline",filter);
    if(mine) q.set("mine","true");
    if(ownerFilter) q.set("owner_id",ownerFilter);
    try{
      const [t,s,p]=await Promise.all([
        api("GET","/api/tasks?"+q.toString()),
        api("GET","/api/stats").catch(()=>null),
        api("GET","/api/pipelines").catch(()=>[]),
      ]);
      setTasks(t||[]); setStats(s); setPipelines(p||[]);
      if(canSeeAll){ api("GET","/api/users").then(u=>setUsers(u||[])).catch(()=>{}); }
      if(user.id){ api("GET","/api/tasks?mine=true").then(mt=>{ const seen=+(localStorage.getItem("th_seen_max")||0); setMineNew((mt||[]).filter(x=>!TERMINAL_ST.includes(x.status)&&x.id>seen).length); }).catch(()=>{}); }
    }catch(e){
      if(!silent) toast.error("โหลดงานไม่สำเร็จ · ลองรีเฟรชอีกครั้ง");
    }finally{
      setLoading(false);
    }
  },[filter,mine,canSeeAll,user.id,ownerFilter]);
  useEffect(()=>{ load(); },[load]);
  // live refresh เงียบ ๆ ทุก 30 วิ + ตอนกลับมาโฟกัสแท็บ (กันข้อมูลค้างเมื่อหลายคนแก้พร้อมกัน)
  useEffect(()=>{ const iv=setInterval(()=>{ if(!document.hidden) load(true); },30000);
    const onFocus=()=>{ if(!document.hidden) load(true); }; window.addEventListener("focus",onFocus);
    return ()=>{ clearInterval(iv); window.removeEventListener("focus",onFocus); }; },[load]);
  const markMineSeen=useCallback(()=>{ api("GET","/api/tasks?mine=true").then(mt=>{ const mx=Math.max(0,...((mt||[]).map(x=>x.id))); localStorage.setItem("th_seen_max",String(mx)); setMineNew(0); }).catch(()=>setMineNew(0)); },[]);

  // deep-link: /app/#<id> → เปิดงานนั้นตรง · sync 2 ทาง (hash ↔ sel)
  useEffect(()=>{
    function fromHash(){ const m=(location.hash||"").match(/#(\d+)/); if(m){ const t=(tasks||[]).find(x=>x.id===+m[1]); if(t) setSel(t); } }
    fromHash(); window.addEventListener("hashchange",fromHash);
    return ()=>window.removeEventListener("hashchange",fromHash);
  },[tasks]);
  // หลัง load/poll → resync งานที่เลือกอยู่ให้เป็นข้อมูลล่าสุด (กัน detail ค้าง)
  useEffect(()=>{ if(sel){ const f=(tasks||[]).find(x=>x.id===sel.id); if(f&&f!==sel) setSel(f); } },[tasks]);
  // keyboard shortcuts (desktop) · / ค้นหา · c/q เพิ่มงาน · j/k เลื่อน · Enter เปิด · x ติ๊กเสร็จ
  useEffect(()=>{
    function onKey(e){
      const tag=(e.target.tagName||"").toLowerCase();
      if(tag==="input"||tag==="textarea"||tag==="select"||e.metaKey||e.ctrlKey||e.altKey) return;
      const list=shownRef.current||[], A=kbActions.current;
      if(e.key==="/"){ e.preventDefault(); searchRef.current&&searchRef.current.focus(); }
      else if(e.key==="c"||e.key==="q"){ if(A.canEdit){ e.preventDefault(); A.setCreating(true); } }
      else if(e.key==="j"||e.key==="ArrowDown"){ e.preventDefault(); setCursor(c=>Math.min((c<0?-1:c)+1,list.length-1)); }
      else if(e.key==="k"||e.key==="ArrowUp"){ e.preventDefault(); setCursor(c=>Math.max((c<=0?0:c)-1,0)); }
      else if(e.key==="Enter"){ const t=list[cursorRef.current]; if(t){ e.preventDefault(); A.openTask(t); } }
      else if(e.key==="x"){ const t=list[cursorRef.current]; if(t&&A.canEdit){ e.preventDefault(); A.toggleDone(t); } }
    }
    window.addEventListener("keydown",onKey); return ()=>window.removeEventListener("keydown",onKey);
  },[]);
  useEffect(()=>{ cursorRef.current=cursor; if(cursor>=0){ const t=shownRef.current[cursor]; if(t){ const el=document.querySelector(`[data-tid="${t.id}"]`); el&&el.scrollIntoView&&el.scrollIntoView({block:"nearest"}); } } },[cursor]);
  const openTask=(t)=>{ setSel(t); try{ history.replaceState(null,"",location.pathname+"#"+t.id); }catch(_){} };
  const closeTask=()=>{ setSel(null); try{ history.replaceState(null,"",location.pathname); }catch(_){} };
  const openTaskById=(id)=>{ setView("board"); try{ location.hash="#"+id; }catch(_){} };

  const visiblePipes = canSeeAll ? pipelines : pipelines.filter(p=>p.key===user.pipeline_key);
  const showSidebar=isDesktop;   // member ก็มี sidebar (เนื้อหาปรับตาม role)
  const curPipe=pipelines.find(p=>p.key===filter); const showKanban=view==="board"&&curPipe&&curPipe.has_stages;
  const reviewRoles=["head_content","admin","superadmin"].includes(user.role);
  const reviewCount=(stats&&stats.by_status&&((stats.by_status.submitted||0)+(stats.by_status.review||0)))||0;
  const reworkCount=(stats&&stats.by_status&&(stats.by_status.returned||0))||0;
  const todayStr=(()=>{const d=new Date();return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`;})();
  const todayCount=tasks.filter(t=>!TERMINAL_ST.includes(t.status)&&t.due&&t.due<=todayStr).length;
  let shown=tasks;
  if(reviewMode) shown=shown.filter(t=>["submitted","review"].includes(t.status));
  else if(reworkMode) shown=shown.filter(t=>t.status==="returned");
  else if(archiveMode) shown=shown.filter(t=>TERMINAL_ST.includes(t.status));   // คลังงาน: เสร็จ+ยกเลิก
  else if(todayMode) shown=shown.filter(t=>!TERMINAL_ST.includes(t.status)&&t.due&&t.due<=todayStr);
  else shown=shown.filter(t=>!TERMINAL_ST.includes(t.status));   // บอร์ดปกติ: ซ่อนงานปิดแล้ว → ดูใน "คลังงาน"
  if(search.trim()) shown=shown.filter(t=>(t.title||"").toLowerCase().includes(search.trim().toLowerCase()));
  if(tagFilter) shown=shown.filter(t=>(t.tags||"").split(",").map(s=>s.trim()).includes(tagFilter));
  // sort
  const PRANK={P0:0,P1:1,P2:2,P3:3};
  const byDue=(a,b)=>(a.due||"9999").localeCompare(b.due||"9999");
  const byPrio=(a,b)=>(PRANK[a.priority]??9)-(PRANK[b.priority]??9);
  shown=[...shown].sort(
    sortBy==="due"?byDue:
    sortBy==="priority"?((a,b)=>byPrio(a,b)||byDue(a,b)):
    sortBy==="updated"?((a,b)=>(b.updated_at||"").localeCompare(a.updated_at||"")):
    /*smart*/((a,b)=>{const ao=!TERMINAL_ST.includes(a.status),bo=!TERMINAL_ST.includes(b.status); if(ao!==bo)return ao?-1:1; const ad=a.due&&a.due<=todayStr,bd=b.due&&b.due<=todayStr; if(ad!==bd)return ad?-1:1; return byPrio(a,b)||byDue(a,b);})
  );
  shownRef.current=shown; kbActions.current={openTask,toggleDone,setCreating,canEdit};
  const bbtn={padding:"6px 12px",borderRadius:999,fontSize:12.5,fontWeight:700,cursor:"pointer",background:"transparent",color:COLORS.inkMuted,border:`1px solid ${COLORS.hairline}`,whiteSpace:"nowrap"};
  const bsel={padding:"6px 10px",borderRadius:999,fontSize:12.5,background:COLORS.card,color:COLORS.inkMuted,border:`1px solid ${COLORS.hairline}`,cursor:"pointer"};
  return (
    <div style={{display:showSidebar?"flex":"block",minHeight:"100vh",background:COLORS.bgGrad}}>
      {showSidebar && <Sidebar user={user} view={view} setView={setView} filter={filter} setFilter={setFilter} pipelines={visiblePipes} counts={(stats&&stats.by_pipeline)||{}} onLogout={onLogout} canSeeAll={canSeeAll} mine={mine} setMine={setMine} reviewRoles={reviewRoles} reviewMode={reviewMode} setReviewMode={setReviewMode} reviewCount={reviewCount} todayMode={todayMode} setTodayMode={setTodayMode} todayCount={todayCount} reworkMode={reworkMode} setReworkMode={setReworkMode} reworkCount={reworkCount} mineNew={mineNew} onMineSeen={markMineSeen} setOwnerFilter={setOwnerFilter} usersList={users} onImpersonate={impersonate} archiveMode={archiveMode} setArchiveMode={setArchiveMode} trashMode={trashMode} setTrashMode={setTrashMode} canDelete={["admin","service","superadmin"].includes(user.role)} search={search} setSearch={setSearch}/>}
      {!showSidebar && drawerOpen && <div onClick={()=>setDrawerOpen(false)} style={{position:"fixed",inset:0,background:"var(--th-overlay-soft)",zIndex:300}}>
        <div onClick={e=>e.stopPropagation()} style={{position:"fixed",left:0,top:0,bottom:0,zIndex:301,boxShadow:"var(--th-shadow-drawer)"}}>
          <Sidebar user={user} view={view} setView={setView} filter={filter} setFilter={setFilter} pipelines={visiblePipes} counts={(stats&&stats.by_pipeline)||{}} onLogout={onLogout} canSeeAll={canSeeAll} mine={mine} setMine={setMine} reviewRoles={reviewRoles} reviewMode={reviewMode} setReviewMode={setReviewMode} reviewCount={reviewCount} todayMode={todayMode} setTodayMode={setTodayMode} todayCount={todayCount} reworkMode={reworkMode} setReworkMode={setReworkMode} reworkCount={reworkCount} mineNew={mineNew} onMineSeen={markMineSeen} setOwnerFilter={setOwnerFilter} usersList={users} onImpersonate={impersonate} archiveMode={archiveMode} setArchiveMode={setArchiveMode} trashMode={trashMode} setTrashMode={setTrashMode} canDelete={["admin","service","superadmin"].includes(user.role)} search={search} setSearch={setSearch} onNav={()=>setDrawerOpen(false)}/>
        </div>
      </div>}
      <div style={{flex:1,minWidth:0,paddingBottom:90}}>
      {/* header */}
      <div style={{padding:"20px 18px 12px",position:"sticky",top:0,background:"var(--th-header-grad)",backdropFilter:"blur(8px)",zIndex:10}}>
        <div style={{display:"flex",justifyContent:"space-between",alignItems:"center"}}>
          <div style={{display:"flex",alignItems:"center",gap:11}}>
            {!showSidebar && <button onClick={()=>setDrawerOpen(true)} title="เมนู" style={{background:COLORS.card,color:COLORS.ink,width:38,height:38,borderRadius:10,fontSize:18,border:`1px solid ${COLORS.hairline}`,cursor:"pointer",flexShrink:0,lineHeight:1}}>☰</button>}
            <div><div style={{fontSize:12,letterSpacing:2,color:COLORS.accentBright,fontWeight:700}}>NEWVERSE</div>
              <div style={{fontSize:20,fontWeight:700}}>สวัสดี {user.name}</div></div>
          </div>
          <div style={{display:"flex",gap:8,alignItems:"center"}}>
            <ThemeToggle/>
            <button onClick={onLogout} style={{background:COLORS.card,color:COLORS.inkMuted,padding:"8px 12px",borderRadius:10,fontSize:13,border:`1px solid ${COLORS.hairline}`}}>ออก</button>
          </div>
        </div>
        {stats && !showSidebar && <div style={{display:"flex",gap:8,marginTop:14,overflowX:"auto"}}>
          <Stat n={stats.total} label="งานทั้งหมด"/>
          <Stat n={stats.overdue} label="เลยกำหนด" warn/>
          {Object.entries(stats.by_status).slice(0,3).map(([k,v])=><Stat key={k} n={v} label={statusTok(k).label}/>)}
        </div>}
      </div>
      {/* view toggle (admin/head · ซ่อนเมื่อมี sidebar) */}
      {canSeeAll && !showSidebar && <div style={{display:"flex",gap:8,padding:"8px 18px 0"}}>
        {[["board","📋 งาน"],["overview","📊 ภาพรวม"]].map(([v,l])=><button key={v} onClick={()=>setView(v)} style={{padding:"6px 14px",borderRadius:999,fontSize:13,fontWeight:700,cursor:"pointer",border:`1px solid ${view===v?COLORS.accent:COLORS.hairline}`,background:view===v?COLORS.accentSoft:"transparent",color:view===v?COLORS.accentBright:COLORS.inkMuted}}>{l}</button>)}
      </div>}
      {canSeeAll && view==="overview" && <Overview onOpenTask={openTaskById} meId={user.id}
        onPickOwner={(id)=>{ setOwnerFilter(id); setView("board"); setFilter("all"); setMine(false); setReviewMode(false); setTodayMode(false); setReworkMode(false); }}
        onKpi={(kind)=>{ setView("board"); setReviewMode(false); setReworkMode(false); setOwnerFilter(null); setTagFilter(""); setMine(false); setFilter("all"); setTodayMode(kind==="overdue"); }}/>}
      {view==="calendar" && <CalendarView tasks={tasks} onOpenTask={openTask} onAddDay={canEdit?(k=>{setCreateDue(k);setCreating(true);}):undefined} onReschedule={canEdit?((id,iso)=>api("PATCH","/api/tasks/"+id,{due:iso}).then(load).catch(e=>alert(e.message))):undefined}/>}
      {view==="week7" && <Week7View tasks={tasks} onOpenTask={openTaskById} canEdit={canEdit} onAddDay={canEdit?(k=>{setCreateDue(k);setCreating(true);}):undefined} onReschedule={canEdit?((id,iso)=>api("PATCH","/api/tasks/"+id,{due:iso}).then(load).catch(e=>alert(e.message))):undefined}/>}
      {view==="activity" && <ActivityView me={user} users={users}/>}
      {view==="availability" && <AvailabilityView me={user} users={users}/>}
      {view==="board" && <>
      {/* filters */}
      <div style={{display:"flex",gap:8,padding:"10px 18px",overflowX:"auto",alignItems:"center"}}>
        {/* nav chips เฉพาะตอนไม่มี sidebar (mobile) · desktop ใช้ sidebar แทน */}
        {!showSidebar && <>
          {canSeeAll && <Chip on={filter==="all"} onClick={()=>setFilter("all")}>ทั้งหมด</Chip>}
          {visiblePipes.map(p=>{ const tk=pipeTok(p.key); return <Chip key={p.key} on={filter===p.key} color={tk.color} onClick={()=>setFilter(p.key)}>{p.name||tk.label}</Chip>; })}
          {user.id && <Chip on={mine} onClick={()=>setMine(m=>!m)}>งานฉัน</Chip>}
        </>}
        <div style={{flex:1}}/>
        {canEdit && !showKanban && <button onClick={()=>{ setSelMode(m=>!m); setPicked(new Set()); }} style={{padding:"6px 12px",borderRadius:999,fontSize:12.5,fontWeight:600,cursor:"pointer",whiteSpace:"nowrap",background:selMode?COLORS.accentSoft:"transparent",color:selMode?COLORS.accentBright:COLORS.inkMuted,border:`1px solid ${selMode?COLORS.accentEdge:COLORS.hairline}`}}>{selMode?"✕ ยกเลิก":"☑︎ เลือกหลายงาน"}</button>}
        {!showKanban && <select value={sortBy} onChange={e=>setSortBy(e.target.value)} title="เรียงลำดับ" style={{padding:"6px 10px",borderRadius:999,background:COLORS.card,border:`1px solid ${COLORS.hairline}`,color:COLORS.inkMuted,fontSize:12.5,cursor:"pointer"}}>
          <option value="smart">⇅ อัจฉริยะ</option><option value="due">⇅ กำหนดส่ง</option><option value="priority">⇅ ความสำคัญ</option><option value="updated">⇅ อัปเดตล่าสุด</option>
        </select>}
        <div style={{display:"flex",alignItems:"center",gap:6,padding:"6px 10px",borderRadius:999,background:COLORS.card,border:`1px solid ${search?COLORS.accentEdge:COLORS.hairline}`,minWidth:150}}>
          <span style={{fontSize:13,color:COLORS.inkSubtle}}>🔍</span>
          <input ref={searchRef} value={search} onChange={e=>setSearch(e.target.value)} placeholder="ค้นหางาน... ( / )" style={{flex:1,minWidth:0,background:"transparent",border:"none",outline:"none",color:COLORS.ink,fontSize:13}}/>
          {search && <button onClick={()=>setSearch("")} style={{background:"transparent",color:COLORS.inkMuted,fontSize:14,cursor:"pointer",lineHeight:1}}>×</button>}
        </div>
      </div>
      {/* board content: kanban (pipeline มี stages) หรือ list + master-detail */}
      {showKanban ? <Kanban pipelineKey={filter} onOpenTask={openTaskById} canEdit={canEdit} user={user} users={users} canSeeAll={canSeeAll}/> : trashMode ? <TrashView list={trashList} onRestore={restoreTask} onPurge={purgeTask}/> : (
      <div style={{display:"flex",gap:16,padding:"6px 18px",maxWidth:isDesktop?1200:"none",margin:"0 auto",alignItems:"flex-start"}}>
        <div style={{flex:isDesktop?"0 0 400px":"1 1 100%",minWidth:0,width:"100%"}}>
          {canEdit && !reviewMode && !reworkMode && !archiveMode && <QuickAdd onAdd={quickAdd}/>}
          {loading ? <div style={{color:COLORS.inkMuted,textAlign:"center",padding:40}}>กำลังโหลด...</div>
            : shown.length===0 ? <div style={{color:COLORS.inkMuted,textAlign:"center",padding:40}}>{reviewMode?"✅ ไม่มีงานรอรีวิว":reworkMode?"✅ ไม่มีงานตีกลับ":archiveMode?"🗄 ยังไม่มีงานในคลัง":todayMode?"🎉 วันนี้เคลียร์หมดแล้ว":search?`ไม่พบงานที่ตรงกับ "${search}"`:"ยังไม่มีงาน · พิมพ์เพิ่มด้านบนได้เลย"}</div>
            : <>{ownerFilter && <div style={{fontSize:12.5,color:COLORS.accentBright,fontWeight:700,marginBottom:10,display:"flex",alignItems:"center",gap:7}}><span>👤</span> งานของ {((users||[]).find(u=>String(u.id)===String(ownerFilter))||{}).name||"คนนี้"} · {shown.length}<div style={{flex:1}}/><button onClick={()=>setOwnerFilter(null)} style={{background:"transparent",color:COLORS.inkMuted,border:`1px solid ${COLORS.hairline}`,borderRadius:999,padding:"3px 10px",fontSize:12,cursor:"pointer"}}>✕ ล้าง</button></div>}
            {reviewMode && <div style={{fontSize:12.5,color:STATUS.review.fg,fontWeight:700,marginBottom:10,display:"flex",alignItems:"center",gap:7}}><span>🛡</span> รอรีวิว · {shown.length} งาน</div>}
            {reworkMode && <div style={{fontSize:12.5,color:STATUS.returned.fg,fontWeight:700,marginBottom:10,display:"flex",alignItems:"center",gap:7}}><span>↩️</span> ตีกลับ ต้องแก้ · {shown.length} งาน</div>}
            {tagFilter && <div style={{fontSize:12.5,color:COLORS.accentBright,fontWeight:700,marginBottom:10,display:"flex",alignItems:"center",gap:7}}><span>🏷</span> #{tagFilter} · {shown.length}<div style={{flex:1}}/><button onClick={()=>setTagFilter("")} style={{background:"transparent",color:COLORS.inkMuted,border:`1px solid ${COLORS.hairline}`,borderRadius:999,padding:"3px 10px",fontSize:12,cursor:"pointer"}}>✕ ล้าง</button></div>}
            {todayMode && <div style={{fontSize:12.5,color:"var(--th-st-orange-fg)",fontWeight:700,marginBottom:10,display:"flex",alignItems:"center",gap:7}}><span>📅</span> วันนี้ + เลยกำหนด · {shown.length} งาน</div>}
            {archiveMode && <div style={{fontSize:12.5,color:COLORS.inkMuted,fontWeight:700,marginBottom:10,display:"flex",alignItems:"center",gap:7}}><span>🗄</span> คลังงาน · เสร็จ+ยกเลิก · {shown.length} งาน</div>}
            {shown.map(t=><TaskCard key={t.id} t={t} onClick={()=>openTask(t)} onToggleDone={canEdit?toggleDone:undefined} focused={cursor>=0&&shown[cursor]&&shown[cursor].id===t.id} selMode={selMode} picked={picked.has(t.id)} onPick={togglePick} onTagClick={setTagFilter} onPatch={canEdit?((tk,body)=>api("PATCH","/api/tasks/"+tk.id,body).then(load).catch(e=>alert(e.message))):undefined}/>)}</>}
        </div>
        {isDesktop && <div style={{flex:"1 1 auto",minWidth:0,position:"sticky",top:150,maxHeight:"calc(100vh - 170px)",overflowY:"auto"}}>
          {sel ? <Detail key={sel.id} task={sel} user={user} canEdit={canEdit} embedded users={users} canSeeAll={canSeeAll} onClose={closeTask} onChanged={load}/>
               : <div style={{color:COLORS.inkMuted,textAlign:"center",padding:"70px 24px",border:`1px dashed ${COLORS.hairline}`,borderRadius:16}}>← เลือกงานทางซ้ายเพื่อดูรายละเอียด</div>}
        </div>}
      </div>
      )}
      </>}
      {/* bulk action bar */}
      {selMode && <div style={{position:"fixed",left:"50%",transform:"translateX(-50%)",bottom:20,zIndex:30,display:"flex",alignItems:"center",gap:8,flexWrap:"wrap",justifyContent:"center",maxWidth:"94vw",background:COLORS.bgElev,border:`1px solid ${COLORS.accentEdge}`,borderRadius:16,padding:"10px 14px",boxShadow:"var(--th-shadow-bar)"}}>
        <span style={{fontSize:13,fontWeight:700,color:COLORS.inkBright,whiteSpace:"nowrap"}}>เลือก {picked.size}</span>
        <button onClick={()=>setPicked(new Set(shown.map(t=>t.id)))} style={bbtn}>ทั้งหมด ({shown.length})</button>
        <span style={{width:1,height:20,background:COLORS.hairline}}/>
        <button disabled={!picked.size} onClick={()=>bulkReschedule(1)} style={bbtn}>พรุ่งนี้</button>
        <button disabled={!picked.size} onClick={()=>bulkReschedule(7)} style={bbtn}>+1 สัปดาห์</button>
        <select value="" onChange={e=>{ if(e.target.value) bulkPatch({priority:e.target.value},"ตั้ง priority"); }} style={bsel}><option value="">Priority…</option>{["P0","P1","P2","P3"].map(p=><option key={p} value={p}>{p}</option>)}</select>
        {canSeeAll && <select value="" onChange={e=>{ if(e.target.value) bulkPatch({owner_id:+e.target.value},"มอบงาน"); }} style={bsel}><option value="">มอบให้…</option>{users.map(u=><option key={u.id} value={u.id}>{u.name}</option>)}</select>}
        <button disabled={!picked.size} onClick={()=>bulkPatch({status:"done"},"ทำเสร็จ")} style={{...bbtn,color:STATUS.done.fg,borderColor:STATUS.done.dot}}>✓ เสร็จ</button>
        <button onClick={exitSel} style={{...bbtn,color:COLORS.inkMuted}}>ปิด</button>
      </div>}
      {/* impersonate banner (สวมบทคนอื่น) · ค้างล่างจอ กันลืม */}
      {isImp() && <div style={{position:"fixed",left:0,right:0,bottom:0,zIndex:100,background:"linear-gradient(90deg,#FF8B47,#E8702A)",color:"#fff",padding:"9px 16px",display:"flex",alignItems:"center",gap:12,justifyContent:"center",flexWrap:"wrap",boxShadow:"var(--th-shadow-fab-up)"}}>
        <span style={{fontSize:13,fontWeight:700}}>👁 กำลังดูในมุมมองของ {user.name} ({user.role}) — ทุกการกระทำทำในนามนี้</span>
        <button onClick={()=>{stopImp();location.reload();}} style={{background:"rgba(0,0,0,.28)",color:"#fff",border:"1px solid rgba(255,255,255,.5)",borderRadius:999,padding:"4px 14px",fontWeight:700,cursor:"pointer",fontSize:12.5,whiteSpace:"nowrap"}}>← กลับเป็น {(realUser()||{}).name||"ตัวเอง"}</button>
      </div>}
      {/* FAB */}
      {canEdit && !selMode && view!=="activity" && <button onClick={()=>setCreating(true)} style={{position:"fixed",right:20,bottom:isImp()?64:24,width:56,height:56,borderRadius:18,background:COLORS.accent,color:"#fff",fontSize:28,boxShadow:`0 8px 24px ${COLORS.accentGlow}`,zIndex:20}}>+</button>}
      {sel && (!isDesktop || showKanban || view==="calendar") && <Detail task={sel} user={user} canEdit={canEdit} users={users} canSeeAll={canSeeAll} onClose={closeTask} onChanged={load}/>}
      {creating && <Create pipelines={canSeeAll?pipelines:visiblePipes} users={users} canSeeAll={canSeeAll}
        defPipeline={filter!=="all"?filter:""} defDue={createDue||(todayMode?bkkToday():"")} defOwner={(mine&&user.id)?user.id:""}
        onClose={()=>{setCreating(false);setCreateDue("");}} onCreated={load}/>}
      {pwOpen && <ChangePw onClose={()=>setPwOpen(false)}/>}
      </div>
    </div>
  );
}
function Stat({n,label,warn}){ return <div style={{background:COLORS.card,border:`1px solid ${COLORS.hairline}`,borderRadius:14,padding:"10px 14px",minWidth:84}}><div style={{fontSize:22,fontWeight:700,color:warn&&n>0?"var(--th-st-red-fg)":COLORS.ink,fontFamily:NUM_FONT}}>{n}</div><div style={{fontSize:11,color:COLORS.inkMuted}}>{label}</div></div>; }
function Chip({children,on,onClick,color}){ return <button onClick={onClick} style={{padding:"8px 14px",borderRadius:999,whiteSpace:"nowrap",fontSize:13,fontWeight:600,background:on?(color?color+"22":COLORS.accentSoft):"transparent",color:on?(color||COLORS.accentBright):COLORS.inkMuted,border:`1px solid ${on?(color||COLORS.accentEdge):COLORS.hairline}`}}>{children}</button>; }

// ---- Activity & Presence (เห็นเฉพาะ superadmin/exec/admin · service เข้าทาง API) ----
function _utcDate(s){ if(!s) return null; const d=new Date(String(s).replace(" ","T")+"Z"); return isNaN(d.getTime())?null:d; }
function _agoTH(s){ const d=_utcDate(s); if(!d) return "—"; const sec=Math.max(0,(Date.now()-d.getTime())/1000);
  if(sec<60) return "เมื่อกี้"; const m=Math.floor(sec/60); if(m<60) return m+" นาทีที่แล้ว";
  const h=Math.floor(m/60); if(h<24) return h+" ชม.ก่อน"; return Math.floor(h/24)+" วันก่อน"; }
function _hhmmTH(s){ const d=_utcDate(s); if(!d) return ""; return d.toLocaleTimeString("th-TH",{hour:"2-digit",minute:"2-digit",hour12:false}); }
function _dmyhmTH(s){ const d=_utcDate(s); if(!d) return ""; return d.toLocaleString("th-TH",{day:"2-digit",month:"2-digit",hour:"2-digit",minute:"2-digit",hour12:false}); }
const _ACT_TH={login:"เข้าระบบ",logout:"ออกจากระบบ",created:"สร้างงาน",status:"เปลี่ยนสถานะ",update:"แก้ไขงาน",
  handoff:"ส่งต่อลงเพจ",review:"รีวิว",approved:"อนุมัติ",returned:"ตีกลับ",published:"เผยแพร่",
  focus_start:"เริ่มโฟกัส",focus_done:"โฟกัสเสร็จ",comment:"คอมเมนต์",assigned:"มอบหมาย",deleted:"ลบงาน",restored:"กู้คืน",
  email_add:"เพิ่มอีเมล",email_remove:"ลบอีเมล",gdoc_created:"สร้าง Google Doc",snooze:"เลื่อน"};
const _SRC_TH={session:"เข้าระบบ",task:"งาน",member:"จัดการสมาชิก"};
const _ALLLOG_ACTIONS=[["login","เข้าระบบ"],["logout","ออกจากระบบ"],["created","สร้างงาน"],["update","แก้ไข"],["handoff","ส่งต่อลงเพจ"],["review","รีวิว"],["focus_start","เริ่มโฟกัส"],["focus_done","โฟกัสเสร็จ"],["comment","คอมเมนต์"],["add_link","เพิ่มลิงก์"],["attach","แนบไฟล์"],["email_add","เพิ่มอีเมล"],["email_remove","ลบอีเมล"]];
function _downloadCSV(filename, rows){
  const esc=v=>{ const s=(v==null?"":String(v)); return /[",\n]/.test(s)?'"'+s.replace(/"/g,'""')+'"':s; };
  const body=rows.map(r=>r.map(esc).join(",")).join("\r\n");
  const blob=new Blob(["﻿"+body],{type:"text/csv;charset=utf-8;"});
  const a=document.createElement("a"); a.href=URL.createObjectURL(blob); a.download=filename;
  document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(a.href);
}

function PresenceDot({name,online,size=32}){
  const ch=((name||"?").trim()[0])||"?";
  return <div style={{position:"relative",flexShrink:0}}>
    <div style={{width:size,height:size,borderRadius:999,background:COLORS.accentSoft,display:"flex",alignItems:"center",justifyContent:"center",color:COLORS.accentBright,fontWeight:700,fontSize:Math.round(size*0.42)}}>{ch}</div>
    {online!=null && <span style={{position:"absolute",right:-1,bottom:-1,width:11,height:11,borderRadius:999,background:online?"var(--th-st-green-dot)":COLORS.inkFaint,border:`2px solid ${COLORS.bg}`}}/>}
  </div>;
}

// ===== Team Weekly Availability (ลา/WFH/เข้าออฟฟิศ · task #34) =====
const _AVL={office:{e:"🟢",l:"เข้าออฟฟิศ"},wfh:{e:"🔵",l:"WFH"},leave:{e:"⚪",l:"ลา"}};
const _AVLSC={office:["var(--th-st-green-fg)","var(--th-st-green-bg)","var(--th-st-green-dot)"],
              wfh:["var(--th-st-blue-fg)","var(--th-st-blue-bg)","var(--th-st-blue-dot)"],
              leave:["var(--th-st-gray-fg)","var(--th-st-gray-bg)","var(--th-st-gray-dot)"]};
const _THMON=["ม.ค.","ก.พ.","มี.ค.","เม.ย.","พ.ค.","มิ.ย.","ก.ค.","ส.ค.","ก.ย.","ต.ค.","พ.ย.","ธ.ค."];
const _avlYmd=x=>x.getFullYear()+"-"+String(x.getMonth()+1).padStart(2,"0")+"-"+String(x.getDate()).padStart(2,"0");
const _avlMonNow=()=>{const x=new Date();x.setDate(x.getDate()-((x.getDay()+6)%7));return _avlYmd(x);};
const _avlShift=(m,n)=>{const x=new Date(m+"T00:00:00");x.setDate(x.getDate()+n);return _avlYmd(x);};
const _avlDnum=d=>parseInt(d.slice(8),10);
const _avlMon=d=>_THMON[new Date(d+"T00:00:00").getMonth()];

function AvailStats(){
  const [range,setRange]=React.useState({from:"",to:""});
  const [data,setData]=React.useState(null); const [err,setErr]=React.useState("");
  const load=React.useCallback(()=>{ setErr(""); const p=new URLSearchParams();
    if(range.from)p.set("date_from",range.from); if(range.to)p.set("date_to",range.to);
    api("GET","/api/availability/stats"+(p.toString()?"?"+p.toString():"")).then(setData).catch(e=>setErr((e&&e.message)||"โหลดไม่ได้"));
  },[range]);
  React.useEffect(()=>{ load(); },[load]);
  const inp={padding:"7px 10px",borderRadius:9,background:COLORS.card,border:`1px solid ${COLORS.hairline}`,color:COLORS.ink,fontSize:12.5,fontFamily:"inherit"};
  return <div>
    <div style={{display:"flex",gap:8,alignItems:"center",flexWrap:"wrap",marginBottom:14}}>
      <span style={{fontSize:12.5,color:COLORS.inkMuted}}>ช่วง</span>
      <input type="date" value={range.from} onChange={e=>setRange(r=>({...r,from:e.target.value}))} style={inp}/>
      <span style={{color:COLORS.inkSubtle}}>→</span>
      <input type="date" value={range.to} onChange={e=>setRange(r=>({...r,to:e.target.value}))} style={inp}/>
      {(range.from||range.to)&&<button onClick={()=>setRange({from:"",to:""})} style={{...inp,cursor:"pointer",color:COLORS.inkMuted}}>✕ เดือนนี้</button>}
      {data&&<span style={{fontSize:12,color:COLORS.inkSubtle}}>{data.from} – {data.to}</span>}
    </div>
    {err?<div style={{color:"var(--th-st-red-fg)",fontSize:13,padding:20,textAlign:"center"}}>{err}</div>
     :!data?<div style={{color:COLORS.inkMuted,padding:20,textAlign:"center"}}>กำลังโหลด…</div>
     :<div style={{overflowX:"auto",border:`1px solid ${COLORS.hairline}`,borderRadius:14}}>
      <table style={{borderCollapse:"collapse",width:"100%",minWidth:420}}>
        <thead><tr>{["ทีม","🟢 เข้าออฟฟิศ","🔵 WFH","⚪ ลา"].map((h,i)=>
          <th key={i} style={{textAlign:i===0?"left":"center",padding:"10px 12px",fontSize:12,color:COLORS.inkMuted,fontWeight:700,borderBottom:`1px solid ${COLORS.hairline}`}}>{h}</th>)}</tr></thead>
        <tbody>{data.users.map(u=><tr key={u.user_id}>
          <td style={{padding:"9px 12px",fontSize:12.5,fontWeight:600,color:COLORS.ink,borderTop:`1px solid ${COLORS.hairlineSoft}`}}>{u.name}</td>
          {["office","wfh","leave"].map(k=><td key={k} style={{padding:"9px 12px",textAlign:"center",fontSize:13,fontWeight:700,fontFamily:MONO_FONT,color:u[k]?_AVLSC[k][0]:COLORS.inkFaint,borderTop:`1px solid ${COLORS.hairlineSoft}`}}>{u[k]}</td>)}
        </tr>)}</tbody>
      </table>
      <div style={{fontSize:11,color:COLORS.inkSubtle,padding:"8px 12px"}}>นับเฉพาะวันที่ลงสถานะ · วันที่ไม่ได้ลง = ถือว่าเข้าออฟฟิศ (ไม่นับ)</div>
    </div>}
  </div>;
}

function AvailabilityView({me, users}){
  const DOW=["จ","อ","พ","พฤ","ศ","ส","อา"], DOWLONG=["จันทร์","อังคาร","พุธ","พฤหัสบดี","ศุกร์","เสาร์","อาทิตย์"];
  const NEXT={office:"wfh",wfh:"leave",leave:"office"};
  const [week,setWeek]=React.useState(_avlMonNow());
  const [data,setData]=React.useState(null); const [err,setErr]=React.useState(""); const [busy,setBusy]=React.useState(false);
  const [tab,setTab]=React.useState("grid");
  const load=React.useCallback(()=>{ setErr(""); api("GET","/api/availability?week="+week).then(setData).catch(e=>setErr((e&&e.message)||"โหลดไม่ได้")); },[week]);
  React.useEffect(()=>{ load(); },[load]);
  async function setDay(day,status,user_id,note){ setBusy(true);
    try{ const r=await api("POST","/api/availability",{day,status,user_id:user_id||undefined,note:note||undefined});
      if(r&&r.warning)toast.error(r.warning); else toast.ok("บันทึกแล้ว"); load();
    }catch(e){ toast.error((e&&e.message)||"บันทึกไม่ได้"); } finally{ setBusy(false); } }
  const rangeT=m=>_avlDnum(m)+" "+_avlMon(m)+" – "+_avlDnum(_avlShift(m,6))+" "+_avlMon(_avlShift(m,6));
  const C={maxWidth:1180,margin:"0 auto",padding:"0 18px 48px"};
  const navBtn={padding:"7px 13px",borderRadius:10,background:COLORS.card,border:`1px solid ${COLORS.hairline}`,color:COLORS.ink,fontSize:14,cursor:"pointer",fontWeight:700};
  const header=(extra)=><div style={{display:"flex",alignItems:"center",gap:10,flexWrap:"wrap",marginBottom:16}}>
      <button style={navBtn} onClick={()=>setWeek(_avlShift(week,-7))}>←</button>
      <div style={{fontSize:15,fontWeight:800,color:COLORS.inkBright,minWidth:160,textAlign:"center"}}>{rangeT(week)}</div>
      <button style={navBtn} onClick={()=>setWeek(_avlShift(week,7))}>→</button>
      {week!==_avlMonNow()&&<button style={{...navBtn,color:COLORS.accentBright,borderColor:COLORS.accentEdge}} onClick={()=>setWeek(_avlMonNow())}>วันนี้</button>}
      {data&&data.locked&&<span style={{fontSize:12,color:"var(--th-st-amber-fg)",fontWeight:700}}>🔒 ผ่านไปแล้ว · ดูได้อย่างเดียว</span>}
      <div style={{flex:1}}/>{extra}
    </div>;
  if(err)return <div style={C}>{header()}<div style={{color:"var(--th-st-red-fg)",textAlign:"center",padding:30}}>{err}</div></div>;
  if(!data)return <div style={C}>{header()}<div style={{color:COLORS.inkMuted,textAlign:"center",padding:30}}>กำลังโหลด…</div></div>;

  if(!data.is_lead){
    const m0=data.me, locked=data.locked;
    return <div style={C}>
      <div style={{fontSize:13,color:COLORS.inkMuted,marginBottom:12}}>ลงสถานะแต่ละวัน · ไม่ลง = เข้าออฟฟิศอัตโนมัติ · ลงล่วงหน้าได้หลายสัปดาห์</div>
      {header()}
      {m0.over_cap&&<div style={{background:"var(--th-st-amber-bg)",border:"1px solid var(--th-st-amber-dot)",borderRadius:12,padding:"10px 14px",marginBottom:14,fontSize:13,color:"var(--th-st-amber-fg)",fontWeight:700}}>⚠️ WFH สัปดาห์นี้ {m0.wfh_count} วัน · เกินเพดาน {data.cap} วัน/สัปดาห์ (เตือนเฉยๆ ไม่ห้าม)</div>}
      <div style={{display:"grid",gridTemplateColumns:"repeat(auto-fill,minmax(150px,1fr))",gap:10}}>
        {data.days.map((d,i)=>{const cur=m0.status[d], isToday=d===data.today;
          return <div key={d} style={{background:COLORS.card,border:`1px solid ${isToday?COLORS.accentEdge:COLORS.hairline}`,borderRadius:14,padding:"12px 12px 14px"}}>
            <div style={{fontSize:13,fontWeight:800,color:COLORS.inkBright}}>{DOWLONG[i]} {isToday&&<span style={{fontSize:10,color:COLORS.accentBright,fontWeight:700}}>· วันนี้</span>}</div>
            <div style={{fontSize:11,color:COLORS.inkSubtle,fontFamily:MONO_FONT,marginBottom:9}}>{_avlDnum(d)} {_avlMon(d)}</div>
            <div style={{display:"flex",flexDirection:"column",gap:5}}>
              {["office","wfh","leave"].map(s=>{const on=cur===s, sc=_AVLSC[s];
                return <button key={s} disabled={locked||busy} onClick={()=>setDay(d,s)} style={{display:"flex",alignItems:"center",gap:7,padding:"7px 9px",borderRadius:9,cursor:locked?"default":"pointer",fontSize:12.5,fontWeight:on?800:600,border:`1px solid ${on?sc[2]:COLORS.hairline}`,background:on?sc[1]:"transparent",color:on?sc[0]:COLORS.inkMuted,opacity:(locked&&!on)?0.4:1}}>{_AVL[s].e} {_AVL[s].l}</button>;})}
            </div>
            {cur==="leave"&&!locked&&<input defaultValue={m0.note[d]||""} onBlur={e=>{const v=e.target.value.trim(); if(v!==(m0.note[d]||""))setDay(d,"leave",null,v);}} placeholder="เหตุผล (สั้นๆ)" style={{width:"100%",marginTop:7,padding:"6px 8px",borderRadius:8,background:COLORS.cardSoft,border:`1px solid ${COLORS.hairline}`,color:COLORS.ink,fontSize:12,boxSizing:"border-box"}}/>}
            {cur==="leave"&&locked&&m0.note[d]&&<div style={{marginTop:7,fontSize:11,color:COLORS.inkMuted}}>📝 {m0.note[d]}</div>}
          </div>;})}
      </div>
    </div>;
  }

  const locked=data.locked;
  const tabBtn=(v,l)=><button onClick={()=>setTab(v)} style={{padding:"7px 16px",borderRadius:999,fontSize:13,fontWeight:700,cursor:"pointer",border:`1px solid ${tab===v?COLORS.accent:COLORS.hairline}`,background:tab===v?COLORS.accentSoft:"transparent",color:tab===v?COLORS.accentBright:COLORS.inkMuted}}>{l}</button>;
  return <div style={C}>
    <div style={{display:"flex",gap:8,marginBottom:14}}>{tabBtn("grid","📅 ตารางทีม")}{tabBtn("stats","📊 สถิติ")}</div>
    {tab==="grid"?<>
      {header(<span style={{fontSize:12.5,color:COLORS.inkMuted}}>⭐ คนเข้าเยอะสุด = {data.best_day?(_avlDnum(data.best_day)+" "+_avlMon(data.best_day)):"—"} · เหมาะนัดประชุม</span>)}
      <div style={{overflowX:"auto",border:`1px solid ${COLORS.hairline}`,borderRadius:14}}>
        <table style={{borderCollapse:"collapse",width:"100%",minWidth:680}}>
          <thead><tr>
            <th style={{textAlign:"left",padding:"10px 12px",position:"sticky",left:0,background:COLORS.cardElev,fontSize:12,color:COLORS.inkMuted,fontWeight:700,minWidth:130,zIndex:1}}>ทีม</th>
            {data.days.map((d,i)=>{const s=data.summary[i], isToday=d===data.today;
              return <th key={d} style={{padding:"8px 6px",textAlign:"center",minWidth:74,background:s.is_best?"var(--th-st-green-bg)":"transparent",borderLeft:`1px solid ${COLORS.hairlineSoft}`}}>
                <div style={{fontSize:12,fontWeight:800,color:isToday?COLORS.accentBright:COLORS.ink}}>{DOW[i]}{s.is_best?" ⭐":""}</div>
                <div style={{fontSize:10,color:COLORS.inkSubtle,fontFamily:MONO_FONT}}>{_avlDnum(d)}</div>
                <div style={{fontSize:10,color:"var(--th-st-green-fg)",fontWeight:700,marginTop:2}}>เข้า {s.office}</div></th>;})}
          </tr></thead>
          <tbody>
            {data.grid.map(row=><tr key={row.user_id}>
              <td style={{padding:"8px 12px",position:"sticky",left:0,background:COLORS.card,fontSize:12.5,fontWeight:600,color:COLORS.ink,borderTop:`1px solid ${COLORS.hairlineSoft}`,zIndex:1}}>
                {row.over_cap&&<span title={"WFH เกิน "+data.cap+" วัน"}>⚠️ </span>}{row.name}</td>
              {data.days.map(d=>{const s=row.status[d], sc=_AVLSC[s];
                return <td key={d} onClick={()=>!locked&&!busy&&setDay(d,NEXT[s],row.user_id)} title={(row.note&&row.note[d])?(_AVL[s].l+" · "+row.note[d]):_AVL[s].l} style={{padding:"7px 4px",textAlign:"center",cursor:locked?"default":"pointer",background:sc[1],borderTop:`1px solid ${COLORS.hairlineSoft}`,borderLeft:`1px solid ${COLORS.hairlineSoft}`}}>
                  <span style={{fontSize:15}}>{_AVL[s].e}</span></td>;})}
            </tr>)}
          </tbody>
        </table>
      </div>
      <div style={{fontSize:11.5,color:COLORS.inkSubtle,marginTop:10}}>🟢 เข้าออฟฟิศ · 🔵 WFH · ⚪ ลา · {locked?"สัปดาห์ผ่านแล้ว แก้ไม่ได้":"คลิกช่องเพื่อเปลี่ยนสถานะลูกทีม (วน 🟢→🔵→⚪)"} · ⚠️ = WFH เกิน {data.cap} วัน/สัปดาห์</div>
    </>:<AvailStats/>}
  </div>;
}

function ActivityView({me, users}){
  const [tab,setTab]=useState("overview");
  const [presence,setPresence]=useState(null), [today,setToday]=useState(null), [feed,setFeed]=useState(null);
  const [scope,setScope]=useState("recent"), [err,setErr]=useState(""), [loading,setLoading]=useState(true);
  const load=useCallback(()=>{
    if(tab!=="overview"){ setLoading(false); return; }   // poll เฉพาะแท็บภาพรวม
    Promise.all([
      api("GET","/api/activity/presence"),
      api("GET","/api/activity/today"),
      api("GET","/api/activity/feed?limit=80&scope="+scope),
    ]).then(([p,t,f])=>{ if(p)setPresence(p); if(t)setToday(t); if(f)setFeed(f); setErr(""); })
      .catch(e=>setErr((e&&e.message)||"โหลดไม่ได้"))
      .finally(()=>setLoading(false));
  },[scope,tab]);
  useEffect(()=>{ load(); const id=setInterval(load,30000); return ()=>clearInterval(id); },[load]);

  const card={background:COLORS.card,border:`1px solid ${COLORS.hairline}`,borderRadius:16,padding:16};
  const head={fontSize:13,fontWeight:800,color:COLORS.inkBright,marginBottom:12,display:"flex",alignItems:"center",gap:8};
  const online=(presence&&presence.users.filter(u=>u.online))||[];
  const tBtn=(v,l)=><button key={v} onClick={()=>setTab(v)} style={{padding:"7px 15px",borderRadius:999,fontSize:13,fontWeight:700,cursor:"pointer",border:`1px solid ${tab===v?COLORS.accent:COLORS.hairline}`,background:tab===v?COLORS.accentSoft:"transparent",color:tab===v?COLORS.accentBright:COLORS.inkMuted}}>{l}</button>;
  const tabBar=<div style={{display:"flex",gap:8,maxWidth:1100,margin:"0 auto",padding:"4px 18px 10px",width:"100%",boxSizing:"border-box"}}>{tBtn("overview","📊 ภาพรวม")}{tBtn("all","🗂 Log ทั้งหมด")}</div>;
  const wrap=(inner)=><div style={{display:"flex",flexDirection:"column"}}>{tabBar}{inner}</div>;

  if(tab==="all") return wrap(<AllLogPanel me={me} users={users}/>);
  if(loading && !presence) return wrap(<div style={{color:COLORS.inkMuted,textAlign:"center",padding:50}}>กำลังโหลด Activity…</div>);
  if(err && !presence) return wrap(<div style={{...card,maxWidth:560,margin:"0 auto",textAlign:"center"}}>
    <div style={{fontSize:15,fontWeight:700,color:COLORS.ink,marginBottom:6}}>ยังเปิด Activity ไม่ได้</div>
    <div style={{fontSize:13,color:COLORS.inkMuted,lineHeight:1.6}}>{err}</div>
    <div style={{fontSize:12,color:COLORS.inkSubtle,marginTop:10}}>ถ้าเพิ่งดีพลอย รอ backend สักครู่แล้วรีเฟรช</div>
  </div>);

  return wrap(<div style={{maxWidth:1100,margin:"0 auto",padding:"0 18px 40px",display:"flex",flexDirection:"column",gap:16}}>
    <div style={card}>
      <div style={head}><span style={{width:9,height:9,borderRadius:999,background:"var(--th-st-green-dot)",boxShadow:"0 0 8px var(--th-st-green-dot)"}}/> ออนไลน์ตอนนี้ · {online.length}</div>
      {online.length===0 ? <div style={{color:COLORS.inkMuted,fontSize:13}}>ยังไม่มีใครออนไลน์ใน 5 นาทีล่าสุด</div>
        : <div style={{display:"flex",flexWrap:"wrap",gap:10}}>
        {online.map(u=><div key={u.id} style={{display:"flex",alignItems:"center",gap:9,background:"var(--th-hover-soft)",border:`1px solid ${COLORS.hairline}`,borderRadius:999,padding:"6px 13px 6px 6px"}}>
          <PresenceDot name={u.name} online={true} size={28}/>
          <div><div style={{fontSize:12.5,fontWeight:700,color:COLORS.ink,lineHeight:1.1}}>{u.name}{me&&u.id===me.id?" (คุณ)":""}</div>
            <div style={{fontSize:10,color:COLORS.inkSubtle}}>{u.role}</div></div>
        </div>)}
      </div>}
    </div>

    <div style={card}>
      <div style={head}>📊 ใครมาทำงานวันนี้ · {today?today.count:0}</div>
      {(!today||today.users.length===0) ? <div style={{color:COLORS.inkMuted,fontSize:13}}>ยังไม่มีใครเริ่มงานวันนี้</div>
        : <div style={{display:"flex",flexDirection:"column"}}>
        {today.users.map(u=><div key={u.id} style={{display:"flex",alignItems:"center",gap:11,padding:"8px 4px",borderBottom:`1px solid ${COLORS.hairlineSoft}`}}>
          <PresenceDot name={u.name} online={u.online} size={32}/>
          <div style={{flex:1,minWidth:0}}>
            <div style={{fontSize:13,fontWeight:700,color:COLORS.ink,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"}}>{u.name}{me&&u.id===me.id?" (คุณ)":""}</div>
            <div style={{fontSize:10.5,color:COLORS.inkSubtle}}>{u.role} · ใช้ล่าสุด {_agoTH(u.last_seen)}</div>
          </div>
          <div style={{display:"flex",gap:6,flexShrink:0}}>
            {u.actions>0 && <span style={{fontFamily:NUM_FONT,fontSize:11,fontWeight:700,color:COLORS.accentBright,background:COLORS.accentSoft,border:`1px solid ${COLORS.accentEdge}`,borderRadius:999,padding:"2px 9px"}}>{u.actions} งาน</span>}
            {u.logins>0 && <span style={{fontFamily:NUM_FONT,fontSize:11,fontWeight:600,color:COLORS.inkMuted,background:"var(--th-hover-soft)",border:`1px solid ${COLORS.hairline}`,borderRadius:999,padding:"2px 9px"}}>เข้า {u.logins}</span>}
          </div>
        </div>)}
      </div>}
    </div>

    <div style={card}>
      <div style={{display:"flex",alignItems:"center",marginBottom:12,gap:8}}>
        <div style={{...head,marginBottom:0,flex:1}}>📜 ความเคลื่อนไหวล่าสุด</div>
        <div style={{display:"flex",gap:6}}>
          {[["recent","ล่าสุด"],["today","วันนี้"]].map(([v,l])=><button key={v} onClick={()=>setScope(v)} style={{padding:"5px 12px",borderRadius:999,fontSize:12,fontWeight:700,cursor:"pointer",border:`1px solid ${scope===v?COLORS.accent:COLORS.hairline}`,background:scope===v?COLORS.accentSoft:"transparent",color:scope===v?COLORS.accentBright:COLORS.inkMuted}}>{l}</button>)}
        </div>
      </div>
      {(!feed||feed.events.length===0) ? <div style={{color:COLORS.inkMuted,fontSize:13}}>ยังไม่มีความเคลื่อนไหว</div>
        : <div style={{display:"flex",flexDirection:"column"}}>
        {feed.events.map((e,i)=>{
          const isSession=e.source==="session";
          const label=_ACT_TH[e.action]||e.action;
          const dot=isSession?(e.action==="logout"?COLORS.inkFaint:"var(--th-st-blue-dot)"):"var(--th-accent)";
          return <div key={i} style={{display:"flex",gap:10,padding:"7px 2px",borderBottom:`1px solid ${COLORS.hairlineSoft}`,alignItems:"baseline"}}>
            <span style={{fontFamily:NUM_FONT,fontSize:11,color:COLORS.inkSubtle,minWidth:40,flexShrink:0}}>{_hhmmTH(e.created_at)}</span>
            <span style={{width:7,height:7,borderRadius:999,background:dot,flexShrink:0,alignSelf:"center"}}/>
            <div style={{flex:1,minWidth:0,fontSize:12.5,lineHeight:1.5}}>
              <span style={{fontWeight:700,color:COLORS.ink}}>{e.imp_by_name||e.user_name||"ระบบ"}</span>{e.imp_by_name && <span style={{fontSize:10,color:"var(--th-accent)",fontWeight:600}}> 👁สวมบท {e.user_name}</span>}
              <span style={{color:COLORS.inkMuted}}> {label}</span>
              {e.task_title && <span style={{color:COLORS.accentBright,fontWeight:600}}> · {e.task_title}</span>}
            </div>
          </div>;
        })}
      </div>}
    </div>
    <div style={{textAlign:"center",fontSize:10.5,color:COLORS.inkSubtle}}>อัปเดตอัตโนมัติทุก 30 วินาที · online = ใช้งานใน 5 นาที · เห็นเฉพาะหัวหน้า</div>
  </div>);
}

function AllLogPanel({me, users}){
  const [f,setF]=useState({user_id:"",source:"all",action:"",date_from:"",date_to:""});
  const [data,setData]=useState(null), [loading,setLoading]=useState(false), [err,setErr]=useState(""), [busy,setBusy]=useState(false);
  const LIMIT=50;
  const qs=(extra)=>{ const p=new URLSearchParams();
    if(f.user_id)p.set("user_id",f.user_id); if(f.source&&f.source!=="all")p.set("source",f.source);
    if(f.action)p.set("action",f.action); if(f.date_from)p.set("date_from",f.date_from); if(f.date_to)p.set("date_to",f.date_to);
    Object.entries(extra||{}).forEach(([k,v])=>p.set(k,v)); return p.toString(); };
  const load=useCallback((off)=>{ setLoading(true);
    api("GET","/api/activity/log?"+qs({limit:LIMIT,offset:off||0}))
      .then(r=>{ if(!r)return; setData(prev=>(off&&prev)?{...r,events:[...prev.events,...r.events]}:r); setErr(""); })
      .catch(e=>setErr((e&&e.message)||"โหลดไม่ได้")).finally(()=>setLoading(false));
  },[f]);
  useEffect(()=>{ load(0); },[load]);

  async function exportCSV(){ setBusy(true);
    try{ const r=await api("GET","/api/activity/log?"+qs({limit:5000,offset:0}));
      const rows=[["เวลา (ไทย)","ผู้ใช้","role","แหล่ง","action","งาน/เป้าหมาย","ip"]];
      (r.events||[]).forEach(e=>rows.push([
        (_utcDate(e.created_at)?_utcDate(e.created_at).toLocaleString("th-TH",{hour12:false}):e.created_at),
        e.imp_by_name?`${e.imp_by_name} (สวมบท ${e.user_name||""})`:(e.user_name||""), e.role||"", _SRC_TH[e.src]||e.src, (_ACT_TH[e.action]||e.action),
        e.task_title||e.target_name||"", e.ip||"" ]));
      _downloadCSV("activity-log-"+(data&&data.total?data.total+"rows-":"")+(f.date_from||"all")+".csv", rows);
      toast.ok("ดาวน์โหลด "+(rows.length-1)+" รายการ");
    }catch(e){ toast.error((e&&e.message)||"export ไม่ได้"); } finally{ setBusy(false); }
  }

  const set=(k,v)=>setF(p=>({...p,[k]:v}));
  const inp={padding:"7px 10px",borderRadius:9,background:COLORS.card,border:`1px solid ${COLORS.hairline}`,color:COLORS.ink,fontSize:12.5,fontFamily:"inherit"};
  const card={background:COLORS.card,border:`1px solid ${COLORS.hairline}`,borderRadius:16,padding:16};
  const hasFilter=f.user_id||f.source!=="all"||f.action||f.date_from||f.date_to;
  const usersSorted=[...(users||[])].sort((a,b)=>(a.name||"").localeCompare(b.name||""));

  return <div style={{maxWidth:1100,margin:"0 auto",padding:"0 18px 40px"}}>
    <div style={{...card,marginBottom:14,display:"flex",flexWrap:"wrap",gap:8,alignItems:"center"}}>
      <select value={f.user_id} onChange={e=>set("user_id",e.target.value)} style={inp}>
        <option value="">ทุกคน</option>
        {usersSorted.map(u=><option key={u.id} value={u.id}>{u.name}</option>)}
      </select>
      <select value={f.source} onChange={e=>set("source",e.target.value)} style={inp}>
        <option value="all">ทุกแหล่ง</option><option value="session">เข้าระบบ</option><option value="task">งาน</option><option value="member">จัดการสมาชิก</option>
      </select>
      <select value={f.action} onChange={e=>set("action",e.target.value)} style={inp}>
        <option value="">ทุก action</option>
        {_ALLLOG_ACTIONS.map(([v,l])=><option key={v} value={v}>{l}</option>)}
      </select>
      <input type="date" value={f.date_from} onChange={e=>set("date_from",e.target.value)} title="ตั้งแต่" style={inp}/>
      <span style={{color:COLORS.inkSubtle,fontSize:12}}>→</span>
      <input type="date" value={f.date_to} onChange={e=>set("date_to",e.target.value)} title="ถึง" style={inp}/>
      {hasFilter && <button onClick={()=>setF({user_id:"",source:"all",action:"",date_from:"",date_to:""})} style={{...inp,cursor:"pointer",color:COLORS.inkMuted}}>✕ ล้าง</button>}
      <div style={{flex:1}}/>
      <button onClick={exportCSV} disabled={busy} style={{padding:"7px 14px",borderRadius:9,fontSize:12.5,fontWeight:700,cursor:busy?"default":"pointer",border:`1px solid ${COLORS.accentEdge}`,background:COLORS.accentSoft,color:COLORS.accentBright,opacity:busy?.6:1}}>{busy?"กำลังสร้าง…":"⬇ Export CSV"}</button>
    </div>

    {data && <div style={{fontSize:12,color:COLORS.inkMuted,margin:"0 4px 10px"}}>พบ <b style={{color:COLORS.ink,fontFamily:NUM_FONT}}>{data.total}</b> รายการ{hasFilter?" (กรองแล้ว)":""}</div>}

    <div style={card}>
      {err ? <div style={{color:"var(--th-st-red-fg)",fontSize:13,textAlign:"center",padding:20}}>{err}</div>
       : (!data||data.events.length===0) ? <div style={{color:COLORS.inkMuted,fontSize:13,textAlign:"center",padding:30}}>{loading?"กำลังโหลด…":"ไม่พบรายการตามเงื่อนไข"}</div>
       : <div style={{display:"flex",flexDirection:"column"}}>
        {data.events.map((e,i)=>{
          const lbl=_ACT_TH[e.action]||e.action;
          const dot=e.src==="session"?(e.action==="logout"?COLORS.inkFaint:"var(--th-st-blue-dot)"):e.src==="member"?"var(--th-st-orange-dot)":"var(--th-accent)";
          const subj=e.task_title||(e.target_name?("→ "+e.target_name):"");
          return <div key={e.src+"-"+e.id+"-"+i} style={{display:"flex",gap:10,padding:"8px 2px",borderBottom:`1px solid ${COLORS.hairlineSoft}`,alignItems:"baseline"}}>
            <span style={{fontFamily:NUM_FONT,fontSize:11,color:COLORS.inkSubtle,minWidth:92,flexShrink:0}}>{_dmyhmTH(e.created_at)}</span>
            <span style={{width:7,height:7,borderRadius:999,background:dot,flexShrink:0,alignSelf:"center"}}/>
            <div style={{flex:1,minWidth:0,fontSize:12.5,lineHeight:1.5}}>
              <span style={{fontWeight:700,color:COLORS.ink}}>{e.imp_by_name||e.user_name||"ระบบ"}</span>{e.imp_by_name && <span style={{fontSize:10,color:"var(--th-accent)",fontWeight:600}}> 👁สวมบท {e.user_name}</span>}
              <span style={{fontSize:10,color:COLORS.inkSubtle}}> · {_SRC_TH[e.src]||e.src}</span>
              <span style={{color:COLORS.inkMuted}}> · {lbl}</span>
              {subj && <span style={{color:COLORS.accentBright,fontWeight:600}}> · {subj}</span>}
            </div>
          </div>;
        })}
      </div>}
      {data && data.has_more && <div style={{textAlign:"center",marginTop:12}}>
        <button onClick={()=>load(data.events.length)} disabled={loading} style={{padding:"8px 20px",borderRadius:999,fontSize:12.5,fontWeight:700,cursor:loading?"default":"pointer",border:`1px solid ${COLORS.hairlineStr}`,background:"transparent",color:COLORS.ink,opacity:loading?.6:1}}>{loading?"กำลังโหลด…":"โหลดเพิ่ม"}</button>
      </div>}
    </div>
  </div>;
}

// ---- root ----
function App(){
  const [user,setU]=useState(getUser());
  const [hydrating,setH]=useState(true);
  // ทุก mount: ยืนยันตัวตนผ่าน /api/me (รองรับ cookie .newverse.cloud ข้าม subdomain) ·
  // มี session = auto-login (ไม่ต้องกรอกรหัส · เช่น login มาจาก newverse.cloud แล้ว) · ไม่มี = แสดงหน้า Login ในตัว
  useEffect(()=>{
    fetch(API+"/api/me",{credentials:"include",headers: token()?{Authorization:"Bearer "+token()}:{}})
      .then(r=> r.ok ? r.json() : null)
      .then(u=>{
        if(u && u.id){ setUser(u); setU(u); }
        else { setToken(null); setUser(null); setU(null); }
      })
      .catch(()=>{ setToken(null); setUser(null); setU(null); })
      .finally(()=>setH(false));
  },[]);
  if(hydrating) return <div style={{minHeight:"100vh",background:"var(--th-bg)",color:"var(--th-ink-muted)",display:"flex",alignItems:"center",justifyContent:"center"}}>กำลังโหลด…</div>;
  if(!user) return <Login onLogin={u=>setU(u)}/>;
  return <><Board user={user} onLogout={()=>{setToken(null);setUser(null);setU(null);}}/><ToastHost/><ConfirmHost/></>;
}
ReactDOM.createRoot(document.getElementById("root")).render(<App/>);
