// Sales Performance — standalone module. Fully separate DB / logic / master
// data (self-contained module). Storage: finx_sales_requests_v1 · finx_sales_drafts
// · finx_sales_desc. Request No. prefix: SAL-YYMM####.
(function(){
// Sales Performance — sales entry form with import + review workflow.

// ============================================================================
//  Request No. — Temporary Draft Ticket System (Sales Performance)
//  Format: SAL-YYMM####  (running starts at 0001, e.g. SAL-26060001)
//
//  Storage model (localStorage):
//    finx_sales_requests_v1 : { reqNo: {record...} }  — committed (Submit) records = "Database"
//    finx_sales_drafts      : [ reqNo, … ]            — reserved-but-not-committed ticket pool
//
//  Goals: no gaps (reuse abandoned numbers) · no duplicates (concurrency guard on commit).
// ============================================================================
function salesPrefix(){
  const now = new Date();
  const yy = String(now.getFullYear()).slice(2);
  const mm = String(now.getMonth()+1).padStart(2,'0');
  return `SAL-${yy}${mm}`;
}
function salesDB(){ try { return JSON.parse(localStorage.getItem('finx_sales_requests_v1') || '{}'); } catch(_){ return {}; } }
function salesDrafts(){ try { return JSON.parse(localStorage.getItem('finx_sales_drafts') || '[]'); } catch(_){ return []; } }
function saveSalesDrafts(arr){ localStorage.setItem('finx_sales_drafts', JSON.stringify(arr)); }
function salesRunOf(no, prefix){ const n = parseInt(no.slice(prefix.length),10); return isNaN(n)?-1:n; }

// Highest running number currently in use (committed OR reserved) for this month.
function salesMaxRun(prefix){
  const db = salesDB(), drafts = salesDrafts();
  let max = 0;   // 0 → first issued number becomes 0001
  Object.keys(db).forEach(k => { if (k.startsWith(prefix)){ const n = salesRunOf(k,prefix); if (n>max) max = n; } });
  drafts.forEach(k => { if (k.startsWith(prefix)){ const n = salesRunOf(k,prefix); if (n>max) max = n; } });
  return max;
}

// Reserve (or reuse) a ticket number when Add Item is pressed.
function reserveSalesNo(){
  const prefix = salesPrefix();
  const db = salesDB();
  const drafts = salesDrafts();
  // 1) Reuse the lowest reserved number for this month that was never committed.
  const reusable = drafts
    .filter(n => n.startsWith(prefix) && !db[n])
    .sort((a,b)=> salesRunOf(a,prefix) - salesRunOf(b,prefix));
  if (reusable.length){
    return reusable[0];
  }
  // 2) Otherwise issue a brand-new number (max + 1, starting at 0001).
  const next = salesMaxRun(prefix) + 1;
  const no = `${prefix}${String(next).padStart(4,'0')}`;
  saveSalesDrafts([...drafts, no]);
  return no;
}

// Commit a record on Submit, with a concurrency guard against duplicates.
// Returns the FINAL request number actually stored.
function commitSalesRecord(reqNo, record, status){
  const prefix = reqNo.slice(0,7); // SAL-YYMM
  const db = salesDB();
  let finalNo = reqNo;
  if (db[finalNo]){
    const next = salesMaxRun(prefix) + 1;
    finalNo = `${prefix}${String(next).padStart(4,'0')}`;
  }
  db[finalNo] = {
    ...record,
    reqNo: finalNo,
    status,
    submittedAt: status==='Submitted' ? new Date().toISOString() : (db[finalNo]?.submittedAt || null),
    savedAt: new Date().toISOString(),
  };
  localStorage.setItem('finx_sales_requests_v1', JSON.stringify(db));
  saveSalesDrafts(salesDrafts().filter(n => n !== reqNo && n !== finalNo));
  return finalNo;
}
// ── XLSX reader (handles stored + deflated entries) ────────────────────────
async function inflateMaybe(bytes, method){
  if (method === 0) return bytes;                 // stored
  if (method === 8){                              // raw deflate
    const ds = new DecompressionStream('deflate-raw');
    const ab = await new Response(new Blob([bytes]).stream().pipeThrough(ds)).arrayBuffer();
    return new Uint8Array(ab);
  }
  throw new Error('ไฟล์ Excel ใช้การบีบอัดที่ไม่รองรับ');
}
function findZipEntries(buf){
  const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
  const entries = {};
  let i = 0;
  while (i + 4 <= buf.length){
    const sig = dv.getUint32(i, true);
    if (sig !== 0x04034b50) break;                // first non-local-header → stop
    const method = dv.getUint16(i+8, true);
    const compSize = dv.getUint32(i+18, true);
    const nameLen = dv.getUint16(i+26, true);
    const extraLen = dv.getUint16(i+28, true);
    const nameStart = i + 30;
    const name = new TextDecoder().decode(buf.subarray(nameStart, nameStart+nameLen));
    const dataStart = nameStart + nameLen + extraLen;
    entries[name] = { method, bytes: buf.subarray(dataStart, dataStart+compSize) };
    i = dataStart + compSize;
  }
  return entries;
}
async function parseXlsx(arrayBuffer){
  const buf = new Uint8Array(arrayBuffer);
  if (!(buf[0]===0x50 && buf[1]===0x4B)) throw new Error('ไฟล์ไม่ใช่รูปแบบ Excel (.xlsx) ที่ถูกต้อง');
  const entries = findZipEntries(buf);
  const dec = new TextDecoder('utf-8');
  // shared strings (if present)
  let shared = [];
  if (entries['xl/sharedStrings.xml']){
    const ssXml = dec.decode(await inflateMaybe(entries['xl/sharedStrings.xml'].bytes, entries['xl/sharedStrings.xml'].method));
    shared = [...ssXml.matchAll(/<si>([\s\S]*?)<\/si>/g)].map(m =>
      [...m[1].matchAll(/<t[^>]*>([\s\S]*?)<\/t>/g)].map(t=>t[1]).join('')
        .replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>')
    );
  }
  // find first worksheet
  const sheetKey = Object.keys(entries).find(k=>/^xl\/worksheets\/sheet\d+\.xml$/.test(k));
  if (!sheetKey) throw new Error('ไม่พบ worksheet ในไฟล์ Excel');
  const sheetXml = dec.decode(await inflateMaybe(entries[sheetKey].bytes, entries[sheetKey].method));
  const unesc = s => s.replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
  const rows = [...sheetXml.matchAll(/<row[^>]*>([\s\S]*?)<\/row>/g)].map(rm=>{
    const cells = {};
    [...rm[1].matchAll(/<c r="([A-Z]+)\d+"([^>]*)>([\s\S]*?)<\/c>/g)].forEach(cm=>{
      const col = cm[1], attrs = cm[2], inner = cm[3];
      const tMatch = /t="([^"]+)"/.exec(attrs);
      const type = tMatch ? tMatch[1] : 'n';
      let val = '';
      if (type === 'inlineStr'){ const t=/<t[^>]*>([\s\S]*?)<\/t>/.exec(inner); val = t?unesc(t[1]):''; }
      else { const v=/<v>([\s\S]*?)<\/v>/.exec(inner); const raw = v?v[1]:''; val = (type==='s') ? (shared[parseInt(raw,10)]||'') : raw; }
      cells[col] = val;
    });
    return cells;
  });
  // Columns: A=Date, B=Sales Channel, C=Target, D=Actual
  const num = (s)=> parseFloat(String(s||'').replace(/[,\s"]/g,''));
  const out = [];
  rows.forEach((c,idx)=>{
    const rawDate      = (c.A||'').toString().trim();
    const channel = (c.B||'').trim();   // Sales Channel — validated against Master
    const estimate     = num(c.C);
    const amt          = num(c.D);
    // Skip header row & fully-empty rows
    if (idx===0 && isNaN(amt) && isNaN(estimate)) return;
    if (channel && (!isNaN(amt) || !isNaN(estimate))){
      out.push({ date: normImportDate(rawDate), rawDate, channel,
        estimate: isNaN(estimate)?0:estimate,
        amount: isNaN(amt)?0:amt,
        rawTarget: (c.C||'').toString(), rawActual: (c.D||'').toString() });
    }
  });
  const header = rows[0] ? ['A','B','C','D'].map(k => (rows[0][k]||'').toString().trim()) : [];
  return { rows: out, count: out.length, header };
}

// Expected Import header (exact order). Used to reject files with swapped/renamed columns.
const IMPORT_HEADER = ['Date','Sales Channel','Target','Actual'];
const MONTH_NAMES = ['January','February','March','April','May','June','July','August','September','October','November','December'];
function validateImportHeader(header){
  const got = (header||[]).map(h => (h||'').trim());
  const wrong = IMPORT_HEADER
    .map((h,i) => ({ expected:h, actual: got[i]||'' }))
    .filter(x => x.actual.toLowerCase() !== x.expected.toLowerCase());
  if (wrong.length){
    throw new Error('หัวคอลัมน์ไม่ตรงกับ Template จำนวน ' + wrong.length + ' คอลัมน์:  ' +
      wrong.map(x => 'ต้องเป็น “'+x.expected+'” (พบ “'+(x.actual||'ว่าง')+'”)').join('   ·   '));
  }
}

// Normalize an imported date cell → YYYY-MM-DD (or '' if unreadable).
// Handles Excel serial numbers, dd/mm/yy, dd/mm/yyyy.
function normImportDate(raw){
  const s = String(raw||'').trim();
  if (!s) return '';
  let y, mm, dd;
  if (/^\d+(\.\d+)?$/.test(s)){            // Excel serial
    const serial = parseFloat(s);
    const d = new Date(Date.UTC(1899,11,30) + Math.round(serial)*86400000);
    if (isNaN(d)) return '';
    y=d.getUTCFullYear(); mm=d.getUTCMonth()+1; dd=d.getUTCDate();
  } else {
    // Text dates: accept ONLY dd/mm/yyyy (4-digit C.E. year). Anything else → '' (invalid).
    const m = s.match(/^(\d{1,2})[\/\-.](\d{1,2})[\/\-.](\d{4})$/);
    if (!m) return '';
    dd=parseInt(m[1],10); mm=parseInt(m[2],10); y=parseInt(m[3],10);
  }
  // Range guard (also rejects Buddhist-era serials/years like 2569).
  if (mm<1||mm>12||dd<1||dd>31||y<1970||y>2200) return '';
  return `${y}-${String(mm).padStart(2,'0')}-${String(dd).padStart(2,'0')}`;
}

// Master Channel Name catalog. Seeds the Sales Channel DB and
// serves as the valid set for the Import file's "Sales Channel" column.
const CHANNEL_TEMPLATE = ['Retail','Online','B2B'];
const CHANNEL_CATS = [...CHANNEL_TEMPLATE];

// ── Sales Channel master catalog (localStorage: finx_sales_desc) ──
function descDB(){ try { return JSON.parse(localStorage.getItem('finx_sales_desc')||'null'); } catch(_){ return null; } }
function saveDescDB(arr){ localStorage.setItem('finx_sales_desc', JSON.stringify(arr)); }
function ensureDescDB(){
  let db = descDB();
  // Seed the default channels ONLY when the master is empty. Never overwrite
  // an existing master (protects channels the user has added/edited/deleted).
  if (!Array.isArray(db) || db.length === 0){
    db = CHANNEL_TEMPLATE.map((name,i)=>({
      item:i+1, channelName:name,
      createDate:'2026-06-01', requester:'thongchai.hoh', timeOfEntry:'09:00:00'
    }));
    saveDescDB(db);
  }
  return db;
}

// Default state: empty (all fields blank, including Request No.).
// Request No. is auto-generated only when user clicks Add Item for the first time.
// Branch is empty until a Responsible Person is selected.
// Local-timezone today as YYYY-MM-DD (avoids UTC off-by-one from toISOString).
function localToday(){ const d=new Date(); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; }

let _recordIdSeq = 0;
const newRecordId = () => 'REC' + Date.now() + (++_recordIdSeq);   // internal-only key, never shown

const initialSales = () => ({
  _id: '',
  reqDate: '',
  companyName: '',         // locked Company name (e.g. เตือนใจพาณิชย์กรุ๊ป)
  fy: '',
  fm: '',
  channel: '',             // selected Channel name (Retail/Online/B2B)
  rows: [],                 // imported sales rows: {date, channel, estimate, amount, entryDate}
});

// Fields cleared when Reset is clicked. ทุกช่องล้างเป็นค่าว่าง
const clearedHeader = (reqNo) => ({
  reqNo,
  reqDate: '',
  companyName: '',
  fy: '',
  fm: '',
});

// Format ISO/date string → DD/MM/YY
function fmtDate(v){
  if (!v) return '—';
  const d = new Date(v);
  if (isNaN(d)) return v;
  return `${String(d.getDate()).padStart(2,'0')}/${String(d.getMonth()+1).padStart(2,'0')}/${String(d.getFullYear()).slice(2)}`;
}
// Format ISO/date string → DD/MM/YYYY (full 4-digit year)
function fmtDateFull(v){
  if (!v) return '—';
  const d = new Date(v);
  if (isNaN(d)) return v;
  return `${String(d.getDate()).padStart(2,'0')}/${String(d.getMonth()+1).padStart(2,'0')}/${d.getFullYear()}`;
}
// Status icon — green check (submitted) or red exclamation circle (pending)
// Sort caret indicator for sortable table headers.
function sortCaret(sort, key){
  const active = sort.key===key;
  return <span style={{marginLeft:5,fontSize:9,opacity:active?1:.28,color:active?'#1f2a8e':'currentColor'}}>{active ? (sort.dir==='asc'?'▲':'▼') : '▲'}</span>;
}
function SalesAmountCell({ value, editable, onCommit, tone }){
  const [focused, setFocused] = React.useState(false);
  const [draft, setDraft] = React.useState('');
  const num = Number(value)||0;
  const display = focused ? draft : (num===0 ? '' : fmt(num));
  // Tone: Target vs Actual shown in different colours; both bold when filled, faded when 0.
  const filled = num !== 0;
  const textColor = !filled ? '#c3c9da' : (tone==='act' ? '#1f2a8e' : '#b45309');
  const weight = filled ? 700 : 500;
  if (!editable){
    return <span style={{fontWeight:weight,color:textColor}}>{num===0 ? '0.00' : fmt(num)}</span>;
  }
  return (
    <input
      className="se-amt-input"
      value={display}
      inputMode="decimal"
      placeholder="0.00"
      onFocus={()=>{ setFocused(true); setDraft(num===0?'':String(num)); }}
      onChange={e=>{ const v=e.target.value.replace(/[^0-9.]/g,''); if((v.match(/\./g)||[]).length<=1) setDraft(v); }}
      onKeyDown={e=>{ if(e.key==='Enter'){ e.preventDefault(); e.target.blur(); } }}
      onBlur={()=>{ setFocused(false); onCommit(draft===''?0:(parseFloat(draft)||0)); }}
      style={{ color:textColor, fontWeight:weight }}/>
  );
}
// Status icon — green check (submitted) or red exclamation circle (pending)
// Status icon — Submitted (green check) / Pending (yellow clock) / Draft (red dot)
function StatusIcon({ status }){
  const map = {
    Submitted:{ title:'Submitted', grad:'linear-gradient(135deg,#34d399,#059669)', ring:'rgba(16,185,129,.14)',
      icon:<path d="M20 6 9 17l-5-5"/>, sw:3.5 },
    Pending:{ title:'Pending · ยังไม่ Submit', grad:'linear-gradient(135deg,#fbbf24,#d97706)', ring:'rgba(245,158,11,.16)',
      icon:<><circle cx="12" cy="12" r="8"/><path d="M12 8v4.5l3 2"/></>, sw:2.4 },
    Draft:{ title:'Draft · บันทึกร่างแล้ว ยังไม่ Submit', grad:'linear-gradient(135deg,#fb7185,#e11d48)', ring:'rgba(244,63,94,.14)',
      icon:<><line x1="12" y1="8" x2="12" y2="13"/><line x1="12" y1="16.5" x2="12.01" y2="16.5"/></>, sw:3 },
  };
  const s = map[status] || map.Pending;
  return (
    <span title={s.title} style={{display:'inline-flex',alignItems:'center',justifyContent:'center',width:24,height:24,borderRadius:'50%',background:s.grad,boxShadow:`0 0 0 3px ${s.ring}`}}>
      <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth={s.sw} strokeLinecap="round" strokeLinejoin="round">{s.icon}</svg>
    </span>
  );
}

function SalesPerformanceView({ onNav, title='Sales Performance', subtitle='บันทึกข้อมูลผลการขาย', detailLabel='Sales Targets', steps=['General','Sales','Review'], generalPaneTitle='Sales Performance' }){
  const clock = useSystemClock();
  const [form, setForm] = useState(()=> initialSales());
  const [locked, setLocked] = useState(false);           // true once Request No. is issued or a saved record is loaded
  const [reqSearch, setReqSearch] = useState('');        // typed value while searching (before lock)
  const [salesUnlocked, setSalesUnlocked] = useState(false);
  const [reviewUnlocked, setReviewUnlocked] = useState(false);
  const [editingId, setEditingId] = useState(null);
  // Whether any saved (non-Canceled) record exists — drives Edit button enable/tooltip.
  const hasSavedRecords = () => {
    try {
      const db = JSON.parse(localStorage.getItem('finx_sales_requests_v1') || '{}');
      return Object.values(db).some(r => (r.status||'')!=='Canceled');
    } catch(_){ return false; }
  };
  // Role gate for editing a Submitted record (uses the delete permission as proxy for privileged edit).
  const canEditSubmitted = () => {
    try {
      const role = localStorage.getItem('finx_active_role') || 'Viewer';
      const perms = (window.loadPermissions && window.loadPermissions()) || {};
      const p = perms[role] || {};
      return p.delete === true || role === 'Admin' || role === 'Manager';
    } catch(_){ return true; }
  };
  // Role gate for permanent (hard) delete.
  const canDelete = () => {
    try {
      const role = localStorage.getItem('finx_active_role') || 'Viewer';
      const perms = (window.loadPermissions && window.loadPermissions()) || {};
      const p = perms[role] || {};
      return p.delete === true || role === 'Admin';
    } catch(_){ return false; }
  };
  const [recordsExist, setRecordsExist] = useState(hasSavedRecords);
  useEffect(()=>{ setRecordsExist(hasSavedRecords()); });
  const salesEditable = salesUnlocked && !reviewUnlocked;   // locked (gray) once Next: Review is pressed; re-editing unlocks
  const canSubmitSales = salesUnlocked && reviewUnlocked;

  // No seed data — the system starts empty. Submitted requests are stored in
  // localStorage (finx_sales_requests_v1) and become searchable afterwards.
  const [step, setStep] = useState(1);
  const [open1, setOpen1] = useState(true);
  const [open2, setOpen2] = useState(true);
  const [search, setSearch] = useState('');
  const [sort, setSort] = useState({key:null, dir:'asc'});   // Sales Targets column sort
  const toggleSort = (key) => setSort(s => s.key===key ? {key, dir: s.dir==='asc'?'desc':'asc'} : {key, dir:'asc'});
  const dirtyRef = useRef(false);
  const [dirty, setDirtyState] = useState(false);   // reactive mirror of dirtyRef (drives button enable state)
  const markDirty = (v=true) => { dirtyRef.current = v; setDirtyState(v); };
  const deletingRef = useRef(false);
  const savingRef = useRef(false);
  const exportingRef = useRef(false);
  const clearingRef = useRef(false);

  const set = (k,v) => { markDirty(true); setForm(f => ({...f, [k]:v})); };

  // Trigger fields: editing any of these AFTER an Import must reset Sales Targets,
  // so the imported rows can never be out of sync with the sales header.
  const TRIGGER_FIELDS = ['reqDate','channel','fm','fy'];  // header fields
  const setGeneral = (k, v) => {
    const hasImported = (form.rows || []).length > 0;
    if (TRIGGER_FIELDS.includes(k) && hasImported && v !== form[k]){
      window.confirmDialog({
        title:<span>Confirm <span style={{color:'#b91c1c'}}>การแก้ไขข้อมูล</span></span>,
        message:<span style={{fontSize:13}}>มีการแก้ไข Sales Performance ข้อมูลใน Sales Targets จะถูกรีเซ็ต โปรดนำเข้าข้อมูลใหม่อีกครั้ง</span>,
        tone:'danger', okLabel:'Confirm & Reset', cancelLabel:'Cancel',
        onOk: ()=>{
          markDirty(true);
          setForm(f => ({...f, [k]:v, rows:[]}));
          window.toast('รีเซ็ต Sales Targets แล้ว · กรุณา Import ข้อมูลใหม่', {tone:'warn', title:'Sales Targets cleared'});
        }
      });
      return;   // wait for confirmation; do not apply the change yet
    }
    set(k, v);
  };

  // Search a previously-saved request by Request No.
  const onSearchReqNo = () => {
    const q = reqSearch.trim().toUpperCase();
    if (!q){ window.toast('กรอกเลขที่ Request No. ที่ต้องการค้นหา', {tone:'info'}); return; }
    const saved = JSON.parse(localStorage.getItem('finx_sales_requests_v1') || '{}');
    if (saved[q]){
      const rec = saved[q];
      setForm({ ...initialSales(), ...rec, rows: rec.rows || [] });
      setLocked(true);
      setSalesUnlocked(true);
      setReviewUnlocked(false);  // loaded record is editable until Next: Review is pressed
      setReqSearch('');
      window.toast(`โหลดรายการ ${q} สำเร็จ`, {tone:'success', title:'พบข้อมูล'});
    } else {
      window.toast(`ไม่พบเลขที่ ${q} ในระบบ`, {tone:'warn', title:'ค้นหาไม่พบ'});
    }
  };
  const setItem = (cat, val) => {
    markDirty(true);
  };
  // Edit an imported row's Amount inline (matched by Item number).
  const updateRowAmount = (item, val) => {
    setForm(f => ({...f, rows: (f.rows||[]).map(r => Number(r.item)===Number(item) ? {...r, amount: val} : r)}));
    markDirty(true);
  };
  // Edit an imported row's Target inline (matched by Item number).
  const updateRowTarget = (item, val) => {
    setForm(f => ({...f, rows: (f.rows||[]).map(r => Number(r.item)===Number(item) ? {...r, estimate: val} : r)}));
    markDirty(true);
  };
  // Remove a single imported row (with confirm).
  const removeRow = (row) => {
    window.confirmDialog({
      title:<span style={{color:'var(--ink)'}}>Delete <span style={{color:'#b91c1c'}}>ลบแถวนี้ ?</span></span>,
      message:<span style={{fontSize:13,display:'flex',alignItems:'center',gap:12}}>
        <span style={{width:38,height:38,borderRadius:99,flexShrink:0,display:'grid',placeItems:'center',background:'rgba(239,68,68,.12)'}}>
          <span style={{width:26,height:26,borderRadius:99,display:'grid',placeItems:'center',background:'linear-gradient(135deg,#f4526e,#dc2640)',boxShadow:'0 2px 6px -2px rgba(220,38,64,.5)'}}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M5 8h14" transform="rotate(-18 12 8)"/><path d="M9.5 5.5l4 1.3 .6-1.6a1.3 1.3 0 0 0-.8-1.6l-1.1-.4a1.3 1.3 0 0 0-1.6.8l-.6 1.5z"/><path d="M6.4 9.6l1.2 10.8a1.6 1.6 0 0 0 1.6 1.4h6.4a1.6 1.6 0 0 0 1.6-1.4l1-9.1"/><path d="M10 12.5l.7 6M14.3 12l-.7 6"/></svg>
          </span>
        </span>
        <span>ต้องการลบรายการ <b className="thai" style={{color:'#1f2a8e'}}>{row.channel||row.expenseGroup||'—'}</b> วันที่ <b className="mono" style={{color:'#1f2a8e'}}>{fmtDateFull(row.date)}</b> ออกจาก Sales Targets หรือไม่ ?</span></span>,
      tone:'danger', okLabel:'Delete', cancelLabel:'Cancel',
      onOk: ()=>{
        setForm(f => ({...f, rows: (f.rows||[]).filter(r => r !== row)}));
        markDirty(true);
        window.toast('ลบรายการแล้ว', {tone:'success', title:'Deleted'});
      }
    });
  };

  // Live totals from imported rows
  const totalTarget = (form.rows||[]).reduce((s,r)=>s+(Number(r.estimate)||0),0);
  const totalActual   = (form.rows||[]).reduce((s,r)=>s+(Number(r.amount)||0),0);
  const total = totalActual;
  const itemCount = (form.rows||[]).length;
  // Filtered rows by search
  const salesFilteredRows = (form.rows||[]).filter(r =>
    !search ||
    ((r.channel||r.expenseGroup)||'').toLowerCase().includes(search.toLowerCase())
  );
  // Optional column sort (click a sortable header)
  const salesSortedRows = React.useMemo(()=>{
    if (!sort.key) return salesFilteredRows;
    const numKeys = { estimate:1, amount:1 };
    const val = (r)=> sort.key in numKeys ? (Number(r[sort.key])||0) : String(r[sort.key]||'');
    const dir = sort.dir==='asc' ? 1 : -1;
    return [...salesFilteredRows].sort((a,b)=>{
      const av=val(a), bv=val(b);
      if (typeof av==='number') return (av-bv)*dir;
      return av.localeCompare(bv,'th')*dir;
    });
  }, [salesFilteredRows, sort]);
  // Submitted status — green if the saved record is Submitted, else pending (red)
  const isSubmittedRecord = (()=>{
    if (!form._id) return false;
    const rec = salesDB()[form._id];
    return !!(rec && rec.status==='Submitted');
  })();
  // Row status for the Sales Targets table: Submitted / Draft (saved as draft) / Pending (in progress, unsaved)
  const rowStatus = (()=>{
    if (!form._id) return 'Pending';
    const rec = salesDB()[form._id];
    if (rec && rec.status==='Submitted') return 'Submitted';
    if (rec && rec.status==='Draft') return 'Draft';
    return 'Pending';
  })();

  // Button enable conditions
  const canSubmit = reviewUnlocked && (!editingId || dirty);
  // Save draft: same rule as Submit (disable Draft edits until Sales Targets change),
  // and always disabled when editing a Submitted record.
  const canSaveDraft = canSubmit && !(isSubmittedRecord && !!editingId);
  // Reset: only meaningful once something has been created or changed.
  const canReset = dirty || (form.rows||[]).length > 0 || !!editingId;
  // Locked header fields: editing a Submitted record allows changing only Sales Targets.
  const submittedLock = isSubmittedRecord && !!editingId;

  // Sales Performance completeness — all fields must have a value
  const SALES_HEADER_FIELDS = ['reqDate','companyName','fy','fm','channel'];
  const missingFields = SALES_HEADER_FIELDS.filter(k => !form[k] || String(form[k]).trim()==='');
  const isGeneralComplete = missingFields.length === 0;
  const fieldLabels = {
    reqDate:'Request Date', companyName:'Company name',
    fy:'Fiscal Year', fm:'Fiscal Month', channel:'Channel name'
  };

  // No auto-save: data is only persisted when the user clicks Save draft or Submit.
  // (dirtyRef stays true so Exit/Edit guards still warn about unsaved work.)

  // Filtered list (search by Thai/English)
  const filteredCats = CHANNEL_CATS.filter(c =>
    !search || c.toLowerCase().includes(search.toLowerCase())
  );

  const onPreview = () => {
    // Cannot preview until an entry is started (Add Item) AND Sales Performance is complete.
    if (!form._id){
      window.toast('กรุณากด Add Item เพื่อเริ่มรายการก่อนดู Preview', {tone:'warn', title:'ยังไม่ได้ Add Item'});
      return;
    }
    if (!isGeneralComplete){
      window.toast('กรุณากรอกข้อมูลใน Sales Performance ให้ครบทุกช่องก่อนดู Preview', {tone:'warn', title:'ข้อมูลไม่ครบ'});
      return;
    }
    window.openModal({
      title:'Preview', size:'lg',
      message: <SalesPreview form={form} total={total} totalTarget={totalTarget} totalActual={totalActual} itemCount={itemCount}/>,
      actions:[ { label:'Close', kind:'primary' } ]
    });
  };
  const onEdit = () => {
    if (!hasSavedRecords()){
      window.toast('ยังไม่มีรายการที่บันทึกไว้ในระบบ', {tone:'warn', title:'ไม่มีรายการให้แก้ไข'}); return;
    }
    const openPicker = () => window.openModal({
      title:'Edit existing request',
      size:'lg',
      message: <EditSeForm onSubmit={(id)=>{
        const saved = JSON.parse(localStorage.getItem('finx_sales_requests_v1') || '{}');
        const rec = saved[id];
        if (!rec){ window.toast('ไม่พบรายการในระบบ', {tone:'warn', title:'ค้นหาไม่พบ'}); return; }
        // Role guard: only privileged roles may edit a Submitted record.
        if ((rec.status||'')==='Submitted' && !canEditSubmitted()){
          const role = (()=>{ try { return localStorage.getItem('finx_active_role')||'Viewer'; } catch(_){ return 'Viewer'; } })();
          window.toast('บทบาท '+role+' ไม่มีสิทธิ์แก้ไขรายการที่ Submit แล้ว', {tone:'warn', title:'ไม่มีสิทธิ์แก้ไข'});
          return;
        }
        // Migrate legacy keys (currency→companyName, company→channel) on load.
        setForm({ ...initialSales(), ...rec, _id:id, rows: rec.rows || [],
          companyName: rec.companyName ?? rec.currency ?? '',
          channel: rec.channel ?? rec.company ?? '' });
        setLocked(true);
        setEditingId(id);
        setSalesUnlocked(true);
        setReviewUnlocked(false);  // editing an existing record — keep editable until Next: Review
        setReqSearch('');
        markDirty(false);
        window.logActivity('Opened for editing ('+(rec.status||'Draft')+') · '+(rec.channel ?? rec.company ?? '')+' · Sales Performance', '');
        window.toast(`โหลดรายการเพื่อแก้ไข · สถานะ: ${rec.status||'Draft'}`, {tone:'success', title:'Edit mode'});
      }}/>
    });
    // Warn before overwriting in-progress work (same guard as Add Item)
    const hasWork = dirtyRef.current || (form.rows||[]).length > 0 || (!!form._id && !editingId);
    if (hasWork){
      window.confirmDialog({
        title:<span style={{color:'#2747c8'}}><span style={{color:'var(--ink)'}}>Confirm </span>การเปลี่ยนรายการ ?</span>,
        message:<span style={{fontSize:13,display:'flex',alignItems:'center',gap:12}}><StatusHalo kind="info"/><span>คุณมีข้อมูลที่ยังไม่ได้บันทึกหากดำเนินการต่อข้อมูลที่สร้างไว้จะถูกยกเลิกทั้งหมด</span></span>,
        tone:'primary', okLabel:'Continue', cancelLabel:'Cancel',
        okStyle:{background:'linear-gradient(135deg,#3a56d4,#1f2a8e)',color:'#fff',border:'none',boxShadow:'0 6px 16px -8px rgba(31,42,142,.6)'},
        onOk: ()=>setTimeout(openPicker, 0)
      });
      return;
    }
    openPicker();
  };
  const onExit = () => {
    const hasWork = dirtyRef.current || !!form._id || (form.rows||[]).length > 0;
    if (!hasWork){
      onNav && onNav('cashflow'); return;
    }
    const itemN = (form.rows||[]).length;
    window.confirmDialog({
      title:<span style={{color:'var(--ink)'}}>Exit <span style={{color:'#b91c1c'}}>ต้องการออกจากหน้านี้ ?</span></span>,
      message:<span style={{fontSize:13,display:'flex',alignItems:'center',gap:12}}><StatusHalo kind="danger"/><span>คุณมีงานค้าง{itemN?(' "'+itemN+' รายการใน Sales Targets"'):''} ข้อมูลที่ยังไม่บันทึกจะหายไป ยืนยันการออกหรือไม่ ?</span></span>,
      tone:'danger', okLabel:'Exit without saving', cancelLabel:'Cancel',
      onOk: ()=>{ doReset(); onNav && onNav('cashflow'); }
    });
  };
  const onSaveDraft = () => {
    if (!reviewUnlocked){
      window.toast('ต้องผ่านครบทั้ง 3 Step ก่อนบันทึกฉบับร่าง', {tone:'warn', title:'Step ไม่ครบ'}); return;
    }
    if (totalTarget <= 0 && totalActual <= 0){ window.toast('Enter at least one sale before saving', {tone:'warn'}); return; }
    if (!form._id){ window.toast('กรุณากด Add Item เพื่อเริ่มรายการก่อน', {tone:'warn'}); return; }
    const targetNo = editingId || form._id;
    const existing = salesDB()[targetNo] || {};
    const isUpdate = existing.status === 'Draft';
    const doSave = ()=>{
      if (savingRef.current) return;   // guard against double-click
      savingRef.current = true;
      window.showLoading('กำลังบันทึกฉบับร่าง…');
      setTimeout(()=>{ try {
        const cleanRows = (form.rows||[]).filter(r => (Number(r.amount)||0) > 0 || (Number(r.estimate)||0) > 0);
        const db = salesDB();
        const cur = db[targetNo] || {};
        db[targetNo] = { ...cur, ...form, _id:targetNo, rows:cleanRows, status:'Draft', savedAt:new Date().toISOString() };
        localStorage.setItem('finx_sales_requests_v1', JSON.stringify(db));
        doReset();
        window.logActivity((isUpdate?'Updated draft':'Saved draft')+' · Sales Performance', '');
        window.toast(isUpdate ? 'อัปเดตฉบับร่างเดิมแล้ว · ฟอร์มถูกล้างพร้อมสร้างรายการใหม่'
                              : 'Draft saved · ฟอร์มถูกล้างพร้อมสร้างรายการใหม่',
                     {tone:'success', title: isUpdate?'Draft updated':'Saved (Draft)', ttl:3600});
      } finally { window.hideLoading(); savingRef.current = false; } }, 240);
    };
    if (isUpdate){
      window.confirmDialog({
        title:<span>Save draft <span style={{color:'#0f766e'}}>บันทึกฉบับร่าง ?</span></span>,
        message:<span style={{fontSize:13,display:'flex',alignItems:'center',gap:12}}>
          <span style={{width:38,height:38,borderRadius:99,flexShrink:0,display:'grid',placeItems:'center',background:'rgba(15,118,110,.14)'}}>
            <span style={{width:26,height:26,borderRadius:99,display:'grid',placeItems:'center',background:'linear-gradient(135deg,#14b8a6,#0f766e)',boxShadow:'0 2px 6px -2px rgba(15,118,110,.5)'}}>
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M21 2v6h-6"/><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M3 22v-6h6"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/></svg>
            </span>
          </span>
          <span>ต้องการบันทึกการเปลี่ยนแปลงแทนที่ฉบับร่างเดิมหรือไม่ ?</span></span>,
        tone:'success', okLabel:'Update draft', cancelLabel:'Cancel',
        onOk: doSave
      });
      return;
    }
    window.confirmDialog({
      title:<span>Save draft <span style={{color:'#0f766e'}}>บันทึกฉบับร่างเข้าระบบ ?</span></span>,
      message:<span style={{fontSize:13,display:'flex',alignItems:'flex-start',gap:12}}>
          <span style={{width:38,height:38,borderRadius:99,flexShrink:0,display:'grid',placeItems:'center',background:'rgba(15,118,110,.14)',marginTop:1}}>
            <span style={{width:26,height:26,borderRadius:99,display:'grid',placeItems:'center',background:'linear-gradient(135deg,#14b8a6,#0f766e)',boxShadow:'0 2px 6px -2px rgba(15,118,110,.5)'}}>
              <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><path d="M17 21v-8H7v8M7 3v5h8"/></svg>
            </span>
          </span>
          <span style={{lineHeight:1.75}}>บันทึกรายการ : <b style={{color:'#b91c1c'}}>{itemCount} Item</b><br/>Total Target : <b style={{color:'#111827'}}>{fmt(totalTarget)} THB</b><br/>Total Actual : <b style={{color:'#111827'}}>{fmt(totalActual)} THB</b></span>
        </span>,
      tone:'success', okLabel:'Save draft', cancelLabel:'Cancel',
      onOk: doSave
    });
    return;
  };
  const doReset = () => {
    setForm(initialSales());
    setLocked(false);
    setSalesUnlocked(false);
    setReviewUnlocked(false);
    setEditingId(null);
    setStep(1);
    setReqSearch('');
    setOpen1(true);
    setOpen2(true);
    setSearch('');
    markDirty(false);
  };
  // Hard-delete: remove the record from the DB entirely (no history kept), but
  // require delete permission. Writes a separate audit-trail entry.
  const onDelete = () => {
    if (!editingId){
      window.toast('ไม่มีรายการให้ลบ · กรุณากด Edit เพื่อโหลดรายการที่บันทึกไว้ก่อน', {tone:'warn', title:'Delete'});
      return;
    }
    const role = (()=>{ try { return localStorage.getItem('finx_active_role')||'Viewer'; } catch(_){ return 'Viewer'; } })();
    if (!canDelete()){
      window.toast('บทบาท '+role+' ไม่มีสิทธิ์ลบรายการถาวร · ต้องมีสิทธิ์ Delete', {tone:'warn', title:'ไม่มีสิทธิ์'});
      return;
    }
    window.confirmDialog({
      title:<span style={{color:'var(--ink)'}}>Delete <span style={{color:'#b91c1c'}}>ยืนยันการลบรายการ ?</span></span>,
      message:<span style={{fontSize:13,display:'flex',alignItems:'center',gap:12}}>
        <span style={{width:38,height:38,borderRadius:99,flexShrink:0,display:'grid',placeItems:'center',background:'rgba(239,68,68,.12)'}}>
          <span style={{width:26,height:26,borderRadius:99,display:'grid',placeItems:'center',background:'linear-gradient(135deg,#f4526e,#dc2640)',boxShadow:'0 2px 6px -2px rgba(220,38,64,.5)'}}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18"/><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/></svg>
          </span>
        </span>
        <span>ต้องการลบรายการนี้หรือไม่ ? รายการจะถูกลบออกจากระบบถาวรและไม่แสดงใน Report</span></span>,
      tone:'danger', okLabel:'Delete', cancelLabel:'Cancel',
      onOk: ()=>{
        if (deletingRef.current) return;   // guard against double-fire
        deletingRef.current = true;
        window.showLoading('กำลังลบรายการ…');
        setTimeout(()=>{ try {
          const db = salesDB();
          const rec = db[editingId];
          if (!rec){ window.toast('ไม่พบรายการใน Database', {tone:'danger'}); return; }
          window.logDeleteAudit({
            recordId: editingId,
            company: rec.channel ?? rec.company ?? '',
            period: [rec.fm, rec.fy].filter(Boolean).join(' '),
            status: rec.status||'',
            rows: (rec.rows||[]).map(r => ({
              date: r.date||'', channel: r.channel||'', estimate: Number(r.estimate)||0,
              amount: Number(r.amount)||0
            }))
          });
          delete db[editingId];
          localStorage.setItem('finx_sales_requests_v1', JSON.stringify(db));
          doReset();
          window.toast('ลบรายการออกจากระบบถาวรแล้ว', {tone:'success', title:'Deleted', ttl:3800});
        } finally { window.hideLoading(); deletingRef.current = false; } }, 240);
      }
    });
  };
  const onSubmit = () => {
    if (!reviewUnlocked){
      window.toast('ต้องผ่านครบทั้ง 3 Step ก่อนกด Submit', {tone:'warn', title:'Step ไม่ครบ'}); return;
    }
    if (totalTarget <= 0 && totalActual <= 0){ window.toast('Enter at least one sale before submitting', {tone:'warn'}); return; }
    if (!form._id){ window.toast('กรุณากด Add Item เพื่อเริ่มรายการก่อน', {tone:'warn'}); return; }
    // Shared validation (same rules as Import) — catches hand-typed / post-import edits
    const rows = form.rows || [];
    const reqDate = (form.reqDate||'').slice(0,10);
    const negNum = rows.some(r => (Number(r.estimate)||0) < 0 || (Number(r.amount)||0) < 0);
    if (negNum){ window.toast('พบค่า Target หรือ Actual ติดลบ — ค่าต้องเป็น 0 หรือมากกว่า', {tone:'danger', title:'ตรวจสอบข้อมูล'}); return; }
    if (form.fm && form.fy){
      const badPeriod = rows.map(r=>r.date).filter(Boolean).some(d => {
        const dt = new Date(d); return MONTH_NAMES[dt.getMonth()] !== form.fm || String(dt.getFullYear()) !== String(form.fy);
      });
      if (badPeriod){ window.toast('วันที่บางรายการไม่ตรงกับงวด Fiscal '+form.fm+' '+form.fy, {tone:'danger', title:'ตรวจสอบข้อมูล'}); return; }
    }
    const isUpdate = !!editingId;
    const alreadySubmitted = isUpdate && (salesDB()[editingId]||{}).status === 'Submitted';
    window.confirmDialog({
      title: isUpdate ? <span>Update <span style={{color:'#1f2a8e'}}>รายการ ?</span></span> : <span>Submit <span style={{color:'#1f2a8e'}}>ส่งข้อมูลเข้าระบบ ?</span></span>,
      message: (alreadySubmitted || isUpdate)
        ? <span style={{fontSize:13,display:'flex',alignItems:'center',gap:12}}><StatusHalo kind="update"/><span>ต้องการบันทึกการเปลี่ยนแปลงแทนที่ข้อมูลเดิมหรือไม่ ?</span></span>
        : <span style={{fontSize:13,display:'flex',alignItems:'flex-start',gap:12}}>
            <span style={{width:38,height:38,borderRadius:99,flexShrink:0,display:'grid',placeItems:'center',background:'rgba(31,42,142,.12)',marginTop:1}}>
              <span style={{width:26,height:26,borderRadius:99,display:'grid',placeItems:'center',background:'linear-gradient(135deg,#4655c9,#1f2a8e)',boxShadow:'0 2px 6px -2px rgba(31,42,142,.55)'}}>
                <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/></svg>
              </span>
            </span>
            <span style={{lineHeight:1.75}}>บันทึกรายการ : <b style={{color:'#b91c1c'}}>{itemCount} Item</b><br/>Total Target : <b style={{color:'#111827'}}>{fmt(totalTarget)} THB</b><br/>Total Actual : <b style={{color:'#111827'}}>{fmt(totalActual)} THB</b></span>
          </span>,
      okLabel: isUpdate ? 'Update' : 'Submit', tone:'primary',
      onOk: ()=>{
        window.showLoading('กำลังบันทึกหรือแก้ไขรายการ…');
        setTimeout(()=>{ try {
        // Step 2 backend guard: persist rows that carry a real value in Target OR Actual.
        const cleanRows = (form.rows||[]).filter(r => (Number(r.amount)||0) > 0 || (Number(r.estimate)||0) > 0);
        const payload = { ...form, rows: cleanRows };
        const targetNo = editingId || form._id;
        const db = salesDB();
        db[targetNo] = { ...payload, _id:targetNo, status:'Submitted', submittedBy: window.CURRENT_USER, savedAt:new Date().toISOString(), submittedAt:new Date().toISOString() };
        localStorage.setItem('finx_sales_requests_v1', JSON.stringify(db));
        doReset();
        window.logActivity(isUpdate?'Updated · Sales Performance':'Submitted · Sales Performance', '');
        window.finxNotifyMe && window.finxNotifyMe({ t:'info', view:'sales',
          title:(isUpdate?'อัพเดทคำขอ ':'ส่งคำขอ ')+targetNo,
          msg:'Sales Performance · '+(payload.company||payload.channel||'—') });
        window.toast((isUpdate?'อัพเดทรายการสำเร็จ':'บันทึกรายการสำเร็จ')+'  · ฟอร์มถูกล้างพร้อมสร้างรายการใหม่',
                     {tone:'success', title: isUpdate?'Updated & saved':'Submitted & saved', ttl:3800});
        } finally { window.hideLoading(); } }, 240);
      }
    });
  };
  const onReset = () => {
    window.confirmDialog({
      title:'Reset form ?',
      message:<span style={{fontSize:13,display:'flex',alignItems:'center',gap:12}}><span style={{width:38,height:38,borderRadius:99,flexShrink:0,display:'grid',placeItems:'center',background:'rgba(37,99,235,.12)'}}><span style={{width:26,height:26,borderRadius:99,flexShrink:0,display:'grid',placeItems:'center',background:'linear-gradient(135deg,#60a5fa,#2563eb)',boxShadow:'0 2px 6px -2px rgba(37,99,235,.5)'}}><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/></svg></span></span><span>ล้างค่าทั้งหมดกลับเป็นค่าเริ่มต้น (รวมถึง Sales Targets)</span></span>,
      tone:'primary', okLabel:'Reset',
      onOk: ()=>{ doReset(); window.toast('Form reset · ล้างค่าทุกช่องแล้ว'); }
    });
  };
  const onAddItem = () => {
    if (editingId){
      window.toast('กำลังแก้ไขรายการที่โหลดมา · ไม่สามารถเริ่มรายการใหม่ได้ · กรุณาบันทึกหรือ Exit ก่อน', {tone:'warn', title:'Add Item'});
      return;
    }
    // Already has an active entry in this form: just notify, keep it locked.
    if (form._id){
      window.toast('กำลังอยู่ระหว่างกรอกรายการอยู่แล้ว', {tone:'info', title:'Add Item'});
      setLocked(true);
      return;
    }
    const issue = () => {
      // Start from a clean form, auto-fill defaults. No Request No. is generated.
      const now = new Date();
      const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
      setForm({
        ...initialSales(),
        _id: newRecordId(),
        reqDate:  localToday(),
        companyName: 'เตือนใจพาณิชย์กรุ๊ป',   // locked company name
        fy:       String(now.getFullYear()),
        fm:       MONTHS[now.getMonth()],
        channel:  '',                       // no default — user must choose
      });
      setLocked(true);
      setReqSearch('');
      setSalesUnlocked(false);
      setReviewUnlocked(false);
      setStep(1);
      markDirty(false);
      window.logActivity('Add Item · Sales Performance', '');
      window.toast('เริ่มรายการใหม่ · กรอกข้อมูลได้เลย', {tone:'success', title:'Add Item'});
    };
    // If the user is mid-entry, confirm before clearing.
    if (dirtyRef.current){
      window.confirmDialog({
        title:<span>Confirm <span style={{color:'#b91c1c'}}>เริ่มรายการใหม่ ?</span></span>,
        message:<span style={{fontSize:13,display:'flex',alignItems:'center',gap:12}}><span style={{width:38,height:38,borderRadius:99,flexShrink:0,display:'grid',placeItems:'center',background:'rgba(245,158,11,.14)'}}><span style={{width:26,height:26,borderRadius:99,display:'grid',placeItems:'center',background:'linear-gradient(135deg,#fbbf24,#d97706)',boxShadow:'0 2px 6px -2px rgba(217,119,6,.5)'}}><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg></span></span><span>หากยืนยันค่าที่กรอกไว้ทั้งหมดจะถูกล้างโดยไม่มีการบันทึก</span></span>,
        tone:'danger', okLabel:'Confirm & Clear', cancelLabel:'Cancel',
        onOk: issue
      });
      return;
    }
    issue();
  };
  const onClearAll = () => {
    const role = (()=>{ try { return localStorage.getItem('finx_active_role')||'Viewer'; } catch(_){ return 'Viewer'; } })();
    const perms = (window.loadPermissions && window.loadPermissions()) || {};
    const canClear = (perms[role]||{}).delete === true || role==='Admin' || role==='Manager';
    if (!canClear){ window.toast('บทบาท '+role+' ไม่มีสิทธิ์ล้างรายการทั้งหมด', {tone:'warn', title:'ไม่มีสิทธิ์'}); return; }
    const n = (form.rows||[]).length;
    if (!n){ window.toast('ไม่มีรายการให้ล้าง', {tone:'warn', title:'Clear all'}); return; }
    if (!salesEditable){ window.toast('ฟอร์มถูกล็อก (ผ่านขั้น Review แล้ว) · กด "Edit" ที่ Sales Targets เพื่อปลดล็อกก่อน', {tone:'warn', title:'ล้างไม่ได้'}); return; }
    window.confirmDialog({
      title:<span style={{color:'var(--ink)'}}>Clear all <span style={{color:'#b91c1c'}}>ล้างรายการทั้งหมด ?</span></span>,
      message:<span style={{fontSize:13,display:'flex',alignItems:'center',gap:12}}>
        <span style={{width:38,height:38,borderRadius:99,flexShrink:0,display:'grid',placeItems:'center',background:'rgba(239,68,68,.12)'}}>
          <span style={{width:26,height:26,borderRadius:99,display:'grid',placeItems:'center',background:'linear-gradient(135deg,#f4526e,#dc2640)',boxShadow:'0 2px 6px -2px rgba(220,38,64,.5)'}}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M5 8h14" transform="rotate(-18 12 8)"/><path d="M9.5 5.5l4 1.3 .6-1.6a1.3 1.3 0 0 0-.8-1.6l-1.1-.4a1.3 1.3 0 0 0-1.6.8l-.6 1.5z"/><path d="M6.4 9.6l1.2 10.8a1.6 1.6 0 0 0 1.6 1.4h6.4a1.6 1.6 0 0 0 1.6-1.4l1-9.1"/><path d="M10 12.5l.7 6M14.3 12l-.7 6"/></svg>
          </span>
        </span>
        <span>ต้องการลบทั้ง <b style={{color:'#1f2a8e'}}>{n} Item</b> ใน Sales Targets หรือไม่ ?</span></span>,
      tone:'danger', okLabel:'Clear', cancelLabel:'Cancel',
      onOk: ()=>{
        if (clearingRef.current) return;   // guard against double-click
        clearingRef.current = true;
        try {
          setForm(f => ({...f, rows: []}));
          window.toast('ล้าง '+n+' รายการแล้ว', {tone:'success', title:'Cleared'});
        } finally { clearingRef.current = false; }
      }
    });
  };
  // New Item → open the Sales Channel popup directly (disabled once a file is imported).
  const onNewChannelItem = () => {
    ensureDescDB();
    const guard = { current: { dirty:false } };   // ChannelItemModal reports unsaved work here
    window.openModal({
      title:'Sales Channel', size:'lg',
      message: <ChannelItemModal requester={window.CURRENT_USER||''} guard={guard}/>,
      actions:[ { label:'Close', kind:'ghost' } ],
      onBeforeClose: ()=>{
        if (!guard.current.dirty) return true;
        return guard.current.requestClose ? guard.current.requestClose() : true;
      }
    });
  };
  // Export the Sales Channel catalog (DB) to Excel.
  const onExportDesc = () => {
    const role = (()=>{ try { return localStorage.getItem('finx_active_role')||'Viewer'; } catch(_){ return 'Viewer'; } })();
    const perms = (window.loadPermissions && window.loadPermissions()) || {};
    const canExport = (perms[role]||{}).export === true || role==='Admin';
    if (!canExport){ window.toast('บทบาท '+role+' ไม่มีสิทธิ์ Export', {tone:'warn', title:'ไม่มีสิทธิ์'}); return; }
    const db = ensureDescDB();
    if (!db.length){ window.toast('ยังไม่มีรายการช่องทางการขายใน Database', {tone:'warn'}); return; }
    if (exportingRef.current) return;   // guard against double-click
    exportingRef.current = true;
    window.showLoading('กำลังสร้างไฟล์ Excel…');
    setTimeout(()=>{ try {
    const head = ['Item','Creation Date','Channel Name','Requester'];
    const rows = db.slice().sort((a,b)=>(Number(a.item)||0)-(Number(b.item)||0))
      .map(r => [String(r.item||''), window.fmtDMY?window.fmtDMY(r.createDate):(r.createDate||''),
        r.channelName||'', r.requester||'']);
    const stamp = localToday();   // YYYY-MM-DD
    window.exportXlsx('SalesChannel_Master_'+stamp+'.xlsx', head, rows, { sheetName:'Sales Channel' });
    window.toast('Export รายการช่องทางการขาย ('+rows.length+' รายการ) สำเร็จ', {tone:'success', title:'Export'});
    } finally { window.hideLoading(); exportingRef.current = false; } }, 240);
  };
  const importFileRef = React.useRef();
  // Import gate: must have Request No. + complete Sales Performance first.
  const onImport = () => {
    if (!form._id){
      window.toast('กรุณากด Add Item เพื่อเริ่มรายการก่อนนำเข้าข้อมูล', {tone:'warn', title:'ยังไม่ได้ Add Item'});
      return;
    }
    if (!isGeneralComplete){
      window.toast('ยังขาด: ' + missingFields.map(k=>fieldLabels[k]).join(', '),
        { tone:'warn', title:'กรอก Sales Performance ให้ครบก่อน Import', ttl:4500 });
      return;
    }
    // 1) Must have pressed "Next: Sales Targets" (section unlocked & editable).
    if (!salesEditable){
      window.toast('กรุณากดปุ่ม "Next: Sales Targets" ในกรอบ Sales Performance ก่อน Import', {tone:'warn', title:'ยังไม่ได้ปลดล็อก Sales Targets'});
      return;
    }
    // 2) Cannot re-import while data already exists in the table.
    if (itemCount > 0){
      window.toast('มีข้อมูลที่ Import ไว้แล้ว ไม่สามารถ Import ซ้ำได้ · กด Clear all ก่อนหากต้องการนำเข้าใหม่', {tone:'warn', title:'Import ซ้ำไม่ได้', ttl:4500});
      return;
    }
    importFileRef.current && importFileRef.current.click();
  };

  // Apply imported rows → form.rows (stamped with entryDate) + persist + result modal.
  // Step 1 of workflow: client-side filter — keep ONLY rows with amount > 0, skip 0/blank.
  const applyImport = (importedRows, sourceCount, dtStr, entryISO) => {
    if (!importedRows.length) throw new Error('ไม่พบรายการที่นำเข้าได้ กรุณาตรวจสอบ Template');
    // Request Date must be set before importing (form-level prerequisite).
    const reqDate = (form.reqDate||'').slice(0,10);
    if (!reqDate){
      throw new Error('กรุณาระบุ Request Date ในหน้า Sales Performance ก่อนนำเข้าไฟล์');
    }
    // ── Full-file validation: scan ALL rows, collect EVERY problem, then report once ──
    const masterSet = new Set(ensureDescDB().map(r => (r.channelName||'').trim()).filter(Boolean));
    const selected  = (form.channel||'').trim();   // Channel name chosen in the form
    const numOk = s => { const v=String(s==null?'':s).replace(/[,\s"]/g,''); return v===''||/^-?\d+(\.\d+)?$/.test(v); };
    const badDateFmt=new Set(), badMaster=new Set(), badSelected=new Set(), badPeriod=new Set();
    let missingDate=0, badNum=0, negNum=0;
    importedRows.forEach(r => {
      const ch  = (r.channel||'').trim();
      const raw = (r.rawDate||'').trim();
      if (!raw) missingDate++;
      else if (!r.date) badDateFmt.add(raw);
      if (ch && !masterSet.has(ch)) badMaster.add(ch);
      if (selected && ch && ch !== selected) badSelected.add(ch);
      if (!numOk(r.rawTarget) || !numOk(r.rawActual)) badNum++;
      if ((Number(r.estimate)||0) < 0 || (Number(r.amount)||0) < 0) negNum++;
      if (form.fm && form.fy && r.date){
        const dt = new Date(r.date);
        if (MONTH_NAMES[dt.getMonth()] !== form.fm || String(dt.getFullYear()) !== String(form.fy)) badPeriod.add(fmtDateFull(r.date));
      }
    });
    const errors = [];
    if (missingDate)      errors.push(`พบ ${missingDate} แถวที่ไม่มีวันที่ — คอลัมน์ Date ต้องกรอกให้ครบ`);
    if (badDateFmt.size)  errors.push('รูปแบบวันที่ไม่ถูกต้อง ต้องเป็น dd/mm/yyyy (ค.ศ.) : '+[...badDateFmt].map(v=>'“'+v+'”').join(' , '));
    if (selected && badSelected.size)
                          errors.push(`Sales Channel ในไฟล์ Import ไม่ตรงกับ Channel Name ที่เลือกไว้ “${selected}” : `+[...badSelected].map(v=>'“'+v+'”').join(' , '));
    else if (badMaster.size)
                          errors.push('Sales Channel ไม่ตรงกับ Channel Name ใน Master Database : '+[...badMaster].map(v=>'“'+v+'”').join(' , '));
    if (badNum)           errors.push(`พบ ${badNum} แถวที่ Target/Actual ไม่ใช่ตัวเลขที่ถูกต้อง`);
    if (negNum)           errors.push(`พบ ${negNum} แถวที่ Target/Actual ติดลบ (ต้อง ≥ 0)`);
    if (badPeriod.size)   errors.push('วันที่ไม่ตรงกับ Period (Fiscal Month) ที่เลือก : '+[...badPeriod].join(', '));
    if (errors.length){
      const head = 'พบข้อผิดพลาด '+errors.length+' ประเภท กรุณาแก้ไขไฟล์ Import ให้ถูกต้อง';
      throw new Error(head+'\n\n'+errors.map((e,i)=>`${i+1}. ${e}`).join('\n'));
    }
    const stamped = importedRows
      .filter(r => r.channel)   // keep every row that has a Sales Channel
      .map((r,i) => ({ ...r, item: r.item ?? (i+1), entryDate: entryISO }));
    if (!stamped.length) throw new Error(`ไฟล์มี ${sourceCount} รายการ แต่ไม่มีข้อมูลให้แสดง`);
    setForm(f => ({...f, rows: stamped}));
    // Do NOT persist to the Database here — a record only becomes Draft/Submitted
    // when the user explicitly clicks Save draft or Submit.
    markDirty(true);
    window.logActivity('Imported Sales · Sales Performance', '');
    window.openModal({
      title:'นำเข้าข้อมูลสำเร็จ!', size:'xs',
      message: <div style={{textAlign:'center',padding:'4px 0'}}>
        <div style={{width:44,height:44,margin:'0 auto 12px',borderRadius:'50%',background:'linear-gradient(135deg,#34d399,#059669)',display:'flex',alignItems:'center',justifyContent:'center',boxShadow:'0 0 0 5px rgba(16,185,129,.12), 0 6px 18px rgba(16,185,129,.45)'}}>
          <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
        </div>
        <div style={{fontWeight:700,fontSize:15,color:'var(--ink)',letterSpacing:'-.01em',marginBottom:3}}>นำเข้าข้อมูล Sales Targets</div>
        <div style={{color:'#64748b',fontSize:12.5,marginBottom:12}}>วันที่ {dtStr}</div>
        <div style={{display:'inline-flex',gap:14,justifyContent:'center',padding:'6px 18px',background:'#f0fdf4',borderRadius:10,border:'1px solid #bbf7d0'}}>
          <div style={{textAlign:'center'}}>
            <div style={{fontSize:18,fontWeight:800,color:'#16a34a',fontFamily:'var(--mono)'}}>{sourceCount}</div>
            <div style={{fontSize:10.5,color:'#15803d',fontWeight:600}}>รายการในไฟล์</div>
          </div>
          <div style={{width:1,background:'#bbf7d0'}}/>
          <div style={{textAlign:'center'}}>
            <div style={{fontSize:18,fontWeight:800,color:'#16a34a',fontFamily:'var(--mono)'}}>{new Set(stamped.map(r=>(r.channel||'').trim())).size}</div>
            <div style={{fontSize:10.5,color:'#15803d',fontWeight:600}}>ประเภทยอดขาย</div>
          </div>
        </div>
      </div>,
      actions:[ { label:'Close', kind:'primary' } ]
    });
  };
  const showImportError = (err) => window.openModal({
    title:<span style={{color:'#b91c1c'}}><span style={{color:'var(--ink)'}}>Import </span>นำเข้าข้อมูลไม่สำเร็จ!</span>, size:'xs',
    message: <div style={{textAlign:'center',padding:'4px 0'}}>
      <div style={{width:44,height:44,margin:'0 auto 12px',borderRadius:'50%',background:'linear-gradient(135deg,#fb7185,#e11d48)',display:'flex',alignItems:'center',justifyContent:'center',boxShadow:'0 0 0 5px rgba(244,63,94,.12), 0 6px 18px rgba(244,63,94,.45)'}}>
        <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
      </div>
      <div style={{fontWeight:700,fontSize:15,color:'var(--ink)',letterSpacing:'-.01em',marginBottom:8}}>เกิดข้อผิดพลาด</div>
      {(()=>{ const full=((err&&err.message)||String(err)); const [head,...rest]=full.split('\n\n'); const body=rest.join('\n\n');
        return (
      <div style={{padding:'10px 14px',background:'#fef2f2',borderRadius:10,border:'1px solid #fecaca',fontSize:12.5,color:'#991b1b',textAlign:'left',wordBreak:'break-word',lineHeight:1.6,whiteSpace:'pre-line'}}>
        {body ? <><div style={{fontWeight:700,marginBottom:8}}>{head}</div><div>{body}</div></> : <span>{head}</span>}
      </div>
        ); })()}
    </div>,
    actions:[ { label:'Close', kind:'primary' } ]
  });

  const handleImportFile = async (e) => {
    const file = e.target.files[0];
    if (!file){ return; }
    // Condition 1: file name must be exactly "Sales_Import_Template"
    const baseName = file.name.replace(/\.[^.]+$/,'').trim();
    if (baseName !== 'Sales_Import_Template'){
      showImportError(new Error('ชื่อไฟล์ไม่ถูกต้อง — ต้องเป็น “Sales_Import_Template” เท่านั้น (ไฟล์ที่เลือก: “'+file.name+'”)'));
      e.target.value = '';
      return;
    }
    const now = new Date();
    const entryISO = now.toISOString();
    const dtStr = `${String(now.getDate()).padStart(2,'0')}/${String(now.getMonth()+1).padStart(2,'0')}/${String(now.getFullYear()).slice(2)} ${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}${now.getHours()<12?'AM':'PM'}`;
    const isXlsx = /\.xlsx$/i.test(file.name);
    window.showLoading('กำลังนำเข้าข้อมูล…', file.name);
    // Yield a frame so the overlay can paint before the (synchronous) parse blocks.
    await new Promise(r => requestAnimationFrame(()=> setTimeout(r, 0)));
    try {
      if (isXlsx){
        const ab = await file.arrayBuffer();
        const { rows, count, header } = await parseXlsx(ab);
        validateImportHeader(header);
        if (!count) throw new Error('ไม่พบข้อมูลในไฟล์ Excel กรุณาตรวจสอบ Template');
        applyImport(rows, count, dtStr, entryISO);
      } else {
        const text = await file.text();
        const lines = text.replace(/^\uFEFF/,'').trim().split(/\r?\n/).filter(l => l.trim());
        validateImportHeader((lines[0]||'').split(',').map(c => c.replace(/"/g,'').trim()));
        const dataLines = lines.filter(l => { const cols=l.split(','); const est=parseFloat((cols[2]||'').replace(/["\s]/g,'')); const act=parseFloat((cols[3]||'').replace(/["\s]/g,'')); return cols.length>=2 && (cols[1]||'').trim() && (!isNaN(est) || !isNaN(act)); });
        if (!dataLines.length) throw new Error('ไม่พบข้อมูลในไฟล์ กรุณาตรวจสอบ Template (Date, Sales Channel, Target, Actual)');
        const imported = [];
        dataLines.forEach(l => {
          const cols = l.split(',').map(c => c.replace(/"/g,'').trim());
          const est = parseFloat(cols[2]); const act = parseFloat(cols[3]);
          if (cols[1] && (!isNaN(est) || !isNaN(act))) imported.push({
            date: normImportDate(cols[0]||''),
            rawDate: cols[0]||'',
            channel: cols[1],
            estimate: isNaN(est)?0:est, amount: isNaN(act)?0:act,
            rawTarget: cols[2]||'', rawActual: cols[3]||'' });
        });
        applyImport(imported, dataLines.length, dtStr, entryISO);
      }
    } catch(err){
      showImportError(err);
    } finally {
      window.hideLoading();
    }
    e.target.value = '';
  };

  return (
    <div className="page">
      <div className="page-head">
        <div>
          <div className="page-title">{title}</div>
          <div className="page-sub">{subtitle}</div>
        </div>
        <div className="row" style={{gap:8}}>
          <button className="btn" onClick={onPreview}><I.Eye size={14}/> Preview</button>
          <button className="btn" onClick={onImport} disabled={itemCount>0}
            title={itemCount>0 ? 'นำเข้าข้อมูลแล้ว — กด Clear all หรือลบรายการให้หมดก่อนจึงจะ Import ใหม่ได้' : ''}
            style={itemCount>0 ? {opacity:.5,cursor:'not-allowed'} : {}}><I.Download size={14}/> Import</button>
          <input ref={importFileRef} type="file" accept=".csv,.txt,.xlsx" style={{display:'none'}} onChange={handleImportFile}/>
          <button className="btn danger" onClick={onExit}><I.Logout size={14}/> Exit</button>
          <button className="btn success" onClick={onSaveDraft} disabled={!canSaveDraft}
            title={!reviewUnlocked ? 'ต้องผ่านครบ 3 Step ก่อน Save draft' : (isSubmittedRecord && !!editingId ? 'รายการที่ Submit แล้ว ไม่สามารถ Save draft ได้' : (editingId && !dirty ? 'ยังไม่มีการแก้ไข — แก้ไขข้อมูลใน Sales Targets ก่อน' : ''))}
            style={{opacity: canSaveDraft ? 1 : 0.5, cursor: canSaveDraft ? 'pointer' : 'not-allowed'}}>
            <I.Save size={14}/> Save draft</button>
          <button className="btn primary" onClick={onSubmit} disabled={!canSubmit}
            title={!reviewUnlocked ? 'ต้องผ่านครบ 3 Step ก่อน Submit' : (editingId && !dirty ? 'ยังไม่มีการแก้ไข — แก้ไขข้อมูลก่อนจึงจะ Submit ได้' : '')}
            style={{opacity: canSubmit ? 1 : 0.5, cursor: canSubmit ? 'pointer' : 'not-allowed'}}>
            <I.Check size={14}/> Submit</button>
        </div>
      </div>

      {/* Stepper — 3 steps only (no Attachments) */}
      <div className="card" style={{padding:'16px 20px',marginBottom:18}}>
        <div className="row" style={{gap:8,alignItems:'center'}}>
          {steps.map((s,i)=>{
            const idx = i+1, active = step===idx, done = step>idx;
            return (
              <React.Fragment key={i}>
                <div className="row" style={{gap:10,cursor:'default'}}>
                  <div style={{
                    width:30,height:30,borderRadius:50,display:'grid',placeItems:'center',fontWeight:700,fontSize:13,
                    background:(active||done)?'linear-gradient(135deg,#2cb8b0,#1f2a8e)':'#eef1f8',
                    color:(active||done)?'#fff':'#7b87a8',
                    boxShadow:active?'0 0 0 4px rgba(31,42,142,.12)':'none',transition:'.2s'
                  }}>{(done || (idx===3 && reviewUnlocked)) ? <I.Check size={14}/> : idx}</div>
                  <div>
                    <div style={{fontSize:11,color:'var(--muted)',letterSpacing:'.06em',textTransform:'uppercase'}}>Step {idx}</div>
                    <div style={{fontWeight:700,fontSize:13}}>{s}</div>
                  </div>
                </div>
                {i<2 && <div style={{flex:1,height:2,background:step>idx?'linear-gradient(90deg,#2cb8b0,#1f2a8e)':'#eef1f8',borderRadius:2,transition:'.3s'}}/>}
              </React.Fragment>
            );
          })}
        </div>
      </div>

      <div className="stack" style={{gap:18}}>
        {/* Sales Performance pane — same as Sales Performance page */}
        <Pane title={generalPaneTitle} open={open1} onToggle={()=>setOpen1(!open1)} accent>
          <div style={{display:'grid',gridTemplateColumns:'calc((100% - 28px)/3) calc((100% - 28px)/3) minmax(0,1fr)',gap:14,alignItems:'flex-end'}}>
            <div className="field">
              <label>Request Date <span className="req">*</span> <span style={{color:'#7b87a8',fontWeight:500,fontSize:11,marginLeft:4}}>(auto)</span></label>
              <div style={{position:'relative'}}>
                <input className="input mono" value={form.reqDate ? fmtDateFull(form.reqDate) : ''} readOnly
                       placeholder="— กด Add Item —"
                       title="Request Date ถูกตั้งเป็นวันที่ปัจจุบันอัตโนมัติ และล็อกไว้"
                       style={{
                         background: form.reqDate ? 'linear-gradient(135deg,#eef1f8,#fff)' : '#fafbff',
                         color: form.reqDate ? 'var(--ink)' : '#9aa3c2',
                         fontWeight:700,paddingRight:32,cursor:'not-allowed'
                       }}/>
                <I.Lock size={14} stroke="#7b87a8"
                  style={{position:'absolute',right:12,top:'50%',transform:'translateY(-50%)'}}/>
              </div>
            </div>
            <div className="field">
              <label>Company name <span className="req">*</span> <span style={{color:'#7b87a8',fontWeight:500,fontSize:11,marginLeft:4}}>(locked)</span></label>
              <div style={{position:'relative'}}>
                <input className="input thai" value={form.companyName} readOnly
                       placeholder="— กด Add Item —"
                       title="Company name ถูกล็อคไว้เป็น เตือนใจพาณิชย์กรุ๊ป"
                       style={{
                         background: form.companyName ? 'linear-gradient(135deg,#eef1f8,#fff)' : '#fafbff',
                         color: form.companyName ? 'var(--ink)' : '#9aa3c2',
                         fontWeight:700,paddingRight:32,cursor:'not-allowed'
                       }}/>
                <I.Lock size={14} stroke="#7b87a8"
                  style={{position:'absolute',right:12,top:'50%',transform:'translateY(-50%)'}}/>
              </div>
            </div>
            <div className="row" style={{gap:8,flexWrap:'wrap',justifyContent:'flex-end'}}>
              <button className="btn success" onClick={onAddItem} disabled={!!editingId || !!form._id}
                title={editingId ? 'กำลังแก้ไขรายการที่โหลดมา — ไม่สามารถเริ่มรายการใหม่ได้' : (form._id ? 'กำลังทำรายการอยู่ — ไม่สามารถกด Add Item ซ้ำได้' : '')}
                style={(editingId || form._id) ? {opacity:.5,cursor:'not-allowed'} : {}}><I.Plus size={14}/> Add Item</button>
              <button className="btn" onClick={onEdit} disabled={!recordsExist || (!!form._id && !editingId)}
                title={(!!form._id && !editingId) ? 'กำลังกรอกรายการใหม่อยู่ — ไม่สามารถ Edit ได้ · กรุณาบันทึกหรือ Exit ก่อน' : (recordsExist ? 'โหลดรายการที่บันทึกไว้มาแก้ไข' : 'ยังไม่มีรายการที่บันทึกไว้ในระบบ')}
                style={{opacity: (recordsExist && !(!!form._id && !editingId)) ? 1 : .5, cursor: (recordsExist && !(!!form._id && !editingId)) ? 'pointer' : 'not-allowed'}}><I.Edit size={14}/> Edit</button>
              <button className="btn danger" onClick={onDelete} disabled={!editingId}
                title={editingId ? 'ลบรายการนี้ (Soft delete)' : 'กด Edit เพื่อโหลดรายการที่บันทึกไว้ก่อนจึงจะลบได้'}
                style={!editingId ? {opacity:.45,cursor:'not-allowed'} : {}}>
                <I.Trash size={14}/> Delete</button>
              <button className="btn primary" onClick={onReset} disabled={!canReset}
                title={!canReset ? 'ยังไม่มีการสร้างรายการหรือแก้ไข' : ''}
                style={!canReset ? {opacity:.5,cursor:'not-allowed'} : {}}><I.Reset size={14}/> Reset</button>
            </div>
          </div>
          <div style={{display:'grid',gridTemplateColumns:'1fr 1fr 1fr',gap:14,marginTop:14}}>
            <div className="field">
              <label>Channel name <span className="req">*</span></label>
              <select className="select" value={form.channel} onChange={e=>setGeneral('channel',e.target.value)}
                disabled={submittedLock}
                title={submittedLock ? 'รายการที่ Submit แล้ว ไม่สามารถเปลี่ยน Channel name ได้' : ''}>
                <option value="">— Select —</option>
                {ensureDescDB().map(r=>r.channelName).filter(Boolean).map(c=><option key={c}>{c}</option>)}
              </select>
            </div>
            <div className="field">
              <label>Fiscal Month <span className="req">*</span></label>
              <select className="select" value={form.fm} disabled={submittedLock} onChange={e=>setGeneral('fm',e.target.value)}>
                <option value="">— Select —</option>
                {['January','February','March','April','May','June','July','August','September','October','November','December'].map(m=><option key={m}>{m}</option>)}
              </select>
            </div>
            <div className="field">
              <label>Fiscal Year <span className="req">*</span></label>
              <select className="select" value={form.fy} disabled={submittedLock} onChange={e=>setGeneral('fy',e.target.value)}>
                <option value="">— Select —</option>
                {['2026','2027','2028'].map(y=><option key={y}>{y}</option>)}
              </select>
            </div>
          </div>
          <div style={{marginTop:18,display:'flex',justifyContent:'flex-end'}}>
            <button className="btn primary" disabled={reviewUnlocked}
                    title={reviewUnlocked ? 'ผ่านขั้น Review แล้ว — กด "← Back" ในกรอบ Sales Targets ก่อนจึงจะแก้ไขได้' : ''}
                    style={reviewUnlocked ? {opacity:.45,cursor:'not-allowed'} : {}}
                    onClick={()=>{
                      if (reviewUnlocked) return;
                      if (!isGeneralComplete){
                        window.toast(
                          'ยังขาด: ' + missingFields.map(k=>fieldLabels[k]).join(', '),
                          { tone:'warn', title:'กรุณากรอกข้อมูลให้ครบทุกช่อง', ttl:4500 }
                        );
                        return;
                      }
                      setSalesUnlocked(true);
                      setReviewUnlocked(false);
                      setStep(2);
                      window.toast('Sales Targets ปลดล็อคแล้ว · เริ่มกรอกยอดขายได้', {tone:'success'});
                    }}>
              Next: {detailLabel} <I.Chev size={12}/>
            </button>
          </div>
        </Pane>

        {/* Sales Targets pane — long list */}
        <Pane title={detailLabel} open={open2} onToggle={()=>setOpen2(!open2)} accent>
          {/* Wrapper that grays out when not on step 2 */}
          <div style={{
            opacity: salesEditable ? 1 : 0.45,
            filter:  salesEditable ? 'none' : 'grayscale(.55)',
            pointerEvents: salesEditable ? 'auto' : 'none',
            transition:'opacity .25s ease, filter .25s ease',
            position:'relative',
          }}>

          {/* Action row */}
          <div className="row between" style={{marginBottom:12,flexWrap:'wrap',gap:10}}>
            <div className="search" style={{width:280}}>
              <I.Search size={14} stroke="#9aa3c2"/>
              <input placeholder="ค้นหา Sales Channel…" value={search} onChange={e=>setSearch(e.target.value)}/>
            </div>
            <span className="chip" style={{marginRight:24}}>{salesFilteredRows.length} of {itemCount} rows</span>
            <div className="row" style={{gap:8}}>
              <button className="btn" onClick={onNewChannelItem} disabled={itemCount>0}
                title={itemCount>0 ? 'มีการ Import ข้อมูลแล้ว — ไม่สามารถสร้างรายการใหม่' : ''}
                style={{background:'linear-gradient(135deg,#ea9a0b,#b45309)',color:'#fff',border:'none',opacity: itemCount>0 ? 0.5 : 1, cursor: itemCount>0 ? 'not-allowed' : 'pointer'}}>
                <I.Plus size={14}/> New Item</button>
              <button className="btn" onClick={onExportDesc}><I.Download size={14}/> Export</button>
              <button className="btn danger" onClick={onClearAll} disabled={itemCount===0 || !salesEditable || !!editingId}
                title={editingId ? 'อยู่ในโหมดแก้ไขรายการ — ไม่สามารถล้างทั้งหมดได้' : ((itemCount>0 && !salesEditable) ? 'ฟอร์มถูกล็อก (ผ่านขั้น Review แล้ว) — ปลดล็อกก่อนจึงจะล้างได้' : '')}
                style={(itemCount===0 || !salesEditable || !!editingId) ? {opacity:.5,cursor:'not-allowed'} : {}}><I.Trash size={14}/> Clear all</button>
            </div>
          </div>

          {/* Sales table — uniform cell typography */}
          <div className="tbl-wrap" style={{border:'none',borderRadius:0}}>
            <table className="t se-exp-table sp-amt">
              <thead>
                <tr>
                  <th style={{width:'11%',textAlign:'center'}}>Date</th>
                  <th className="se-sort" style={{width:'12%',textAlign:'center'}} onClick={()=>toggleSort('channel')}>Sales Channel{sortCaret(sort,'channel')}</th>
                  <th className="num se-sort" style={{width:'12%'}} onClick={()=>toggleSort('estimate')}>Target{sortCaret(sort,'estimate')}</th>
                  <th className="num se-sort" style={{width:'12%'}} onClick={()=>toggleSort('amount')}>Actual{sortCaret(sort,'amount')}</th>
                  <th className="num" style={{width:'12%'}}>Variance</th>
                  <th className="num" style={{width:'12%',paddingRight:36}}>Achievement</th>
                  <th style={{width:'14%',textAlign:'center'}}>Entry Date</th>
                  <th style={{textAlign:'center',width:'9%'}}>Status</th>
                  {salesEditable && <th style={{textAlign:'center',width:44}}></th>}
                </tr>
              </thead>
              <tbody>
                {salesSortedRows.map((r,i)=>(
                  <tr key={r.item||r.channel||i} style={{animation:`fadein .25s ${Math.min(i,30)*25}ms both`}}>
                    <td className="mono" style={{textAlign:'center'}}>{fmtDateFull(r.date)}</td>
                    <td className="thai" style={{textAlign:'center'}} title={r.channel||r.expenseGroup||''}>{r.channel||r.expenseGroup||'—'}</td>
                    <td className="num">
                      <SalesAmountCell value={r.estimate} editable={salesEditable} tone="est"
                        onCommit={val => updateRowTarget(r.item, val)}/>
                    </td>
                    <td className="num">
                      <SalesAmountCell value={r.amount} editable={salesEditable} tone="act"
                        onCommit={val => updateRowAmount(r.item, val)}/>
                    </td>
                    {(()=>{ const est=Number(r.estimate)||0, act=Number(r.amount)||0, varc=act-est;
                      return <td className="num mono" style={{fontWeight:700,color:varc===0?'#c3c9da':(varc>0?'#059669':'#dc2626')}}>{varc===0?fmt(0):(varc>0?'+':'−')+fmt(Math.abs(varc))}</td>; })()}
                    {(()=>{ const est=Number(r.estimate)||0, act=Number(r.amount)||0, pct=est>0?act/est*100:0;
                      return <td className="num mono" style={{fontWeight:700,paddingRight:36,color:est===0?'#c3c9da':(pct>=100?'#059669':pct>=80?'#d97706':'#dc2626')}}>{est===0?'—':(pct>=100?'↑ ':'↓ ')+pct.toFixed(2)+'%'}</td>; })()}
                    <td className="mono" style={{textAlign:'center'}}>{fmtDateFull(form.reqDate)}</td>
                    <td style={{textAlign:'center'}}><StatusIcon status={rowStatus}/></td>
                    {salesEditable && <td style={{textAlign:'center'}}>
                      <button className="se-row-del" title="ลบแถวนี้" onClick={()=>removeRow(r)}>
                        <svg className="se-trash" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round"><path d="M6 7v13a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7"/><g className="lid"><path d="M4 7h16"/><path d="M9 7V4.5a1.5 1.5 0 0 1 1.5-1.5h3A1.5 1.5 0 0 1 15 4.5V7"/></g><path d="M10 11.5v6M14 11.5v6"/></svg>
                      </button>
                    </td>}
                  </tr>
                ))}
                {salesSortedRows.length===0 && (
                  <tr><td colSpan="9" style={{textAlign:'center',padding:40,color:'var(--muted)'}}>
                    <div style={{display:'flex',flexDirection:'column',alignItems:'center',gap:8}}>
                      <I.Download size={28} stroke="#cfd5e8"/>
                      <div style={{fontWeight:600,color:'var(--ink-2)'}}>{search ? `ไม่พบรายการที่ตรงกับ "${search}"` : 'ยังไม่มีข้อมูล · กดปุ่ม Import เพื่อนำเข้าจาก Template'}</div>
                    </div>
                  </td></tr>
                )}
              </tbody>
            </table>
          </div>

          {/* Total bar */}
          <div style={{
            marginTop:18,padding:'14px 18px',
            background:'linear-gradient(135deg,#1f2a8e,#2e3bbf)',color:'#fff',
            borderRadius:12,display:'flex',justifyContent:'space-between',alignItems:'center',
            boxShadow:'0 12px 30px -16px rgba(31,42,142,.6)'
          }}>
            <div style={{display:'flex',gap:34,alignItems:'center'}}>
              <div>
                <div style={{fontSize:11,letterSpacing:'.1em',textTransform:'uppercase',opacity:.7}}>Target</div>
                <div className="mono" style={{fontSize:24,fontWeight:800,marginTop:2}}>{fmt(totalTarget)} <span style={{fontSize:13,opacity:.7}}>THB</span></div>
              </div>
              <div style={{width:1,alignSelf:'stretch',background:'rgba(255,255,255,.22)'}}/>
              <div>
                <div style={{fontSize:11,letterSpacing:'.1em',textTransform:'uppercase',opacity:.7}}>Actual</div>
                <div className="mono" style={{fontSize:24,fontWeight:800,marginTop:2}}>{fmt(totalActual)} <span style={{fontSize:13,opacity:.7}}>THB</span></div>
              </div>
            </div>
            <div style={{display:'flex',gap:30,alignItems:'center'}}>
              <div style={{textAlign:'right'}}>
                <div style={{fontSize:11,letterSpacing:'.1em',textTransform:'uppercase',opacity:.7}}>Variance</div>
                {(()=>{ const varc=totalActual-totalTarget;
                  return <div className="mono" style={{fontSize:24,fontWeight:800}}>{(varc>0?'+':varc<0?'−':'')+fmt(Math.abs(varc))} <span style={{fontSize:12,opacity:.7}}>THB</span></div>; })()}
              </div>
              <div style={{width:1,alignSelf:'stretch',background:'rgba(255,255,255,.22)'}}/>
              <div style={{textAlign:'right'}}>
                <div style={{fontSize:11,letterSpacing:'.1em',textTransform:'uppercase',opacity:.7}}>Achievement</div>
                {(()=>{ const pct=totalTarget>0?(totalActual-totalTarget)/totalTarget*100:0;
                  return <div className="mono" style={{fontSize:24,fontWeight:800}}>{(pct>0?'+':pct<0?'−':'')+Math.abs(pct).toFixed(2)+'%'}</div>; })()}
              </div>
              <div style={{width:1,alignSelf:'stretch',background:'rgba(255,255,255,.22)'}}/>
              <div style={{textAlign:'right'}}>
                <div style={{fontSize:11,letterSpacing:'.1em',textTransform:'uppercase',opacity:.7}}>Rows</div>
                <div className="mono" style={{fontSize:24,fontWeight:800}}>{itemCount}</div>
              </div>
            </div>
          </div>

          </div>{/* /lock wrapper — buttons below always clickable */}

          <div style={{marginTop:18,display:'flex',justifyContent:'space-between'}}>
            <button className="btn" onClick={()=>{
              if (reviewUnlocked){
                // Step 3 → Step 2: unlock Expenses for editing again
                setReviewUnlocked(false);
                setStep(2);
                window.toast('กลับมาแก้ Sales Targets ได้อีกครั้ง', {tone:'info', title:'กลับสู่ Step 2'});
              } else {
                // Step 2 → Step 1: lock Expenses, return to General
                setSalesUnlocked(false);
                setReviewUnlocked(false);
                setStep(1);
                window.toast('กลับไปแก้ General แล้ว · Sales Targets ถูกล็อกชั่วคราว', {tone:'warn', title:'กลับสู่ Step 1'});
              }
            }}>← Back</button>
            <button className="btn primary" disabled={itemCount===0}
              style={itemCount===0 ? {opacity:.45,cursor:'not-allowed'} : {}}
              title={itemCount===0 ? 'ต้อง Add Item / Import ข้อมูลใน Sales Targets ก่อน' : ''}
              onClick={()=>{
              if (itemCount===0){ window.toast('กรุณา Add Item หรือ Import ข้อมูลใน Sales Targets ก่อน', {tone:'warn', title:'ยังไม่มีข้อมูล'}); return; }
              if (!salesUnlocked){ window.toast('กรุณากด "Next: Sales Targets" ก่อน', {tone:'warn'}); return; }
              if (!isGeneralComplete){ window.toast('ยังขาด: ' + missingFields.map(k=>fieldLabels[k]).join(', '), {tone:'warn', title:'กรอก Sales Performance ให้ครบก่อนเข้า Review', ttl:4500}); return; }
              setReviewUnlocked(true);
              setStep(3);
              onPreview();
            }}>Next: Review <I.Chev size={12}/></button>
          </div>
        </Pane>
      </div>

      <div style={{marginTop:18,display:'flex',justifyContent:'space-between',color:'var(--muted)',fontSize:12,fontStyle:'italic'}}>
        <span>{dirtyRef.current ? 'Unsaved changes…' : 'All changes saved'}</span>
        <span>{clock}</span>
      </div>
    </div>
  );
}

function SumBox({ label, value, sub, tone, big, raw }){
  return (
    <div style={{
      background:'#fff',border:'1px solid var(--line)',borderRadius:12,padding:'12px 14px',
      borderLeft:`4px solid ${tone}`
    }}>
      <div style={{fontSize:11.5,color:'var(--muted)',fontWeight:600,marginBottom:4}}>{label}</div>
      <div className="mono" style={{fontSize:big?22:18,fontWeight:800,color:tone,letterSpacing:'-.01em'}}>
        {raw ? value : fmt(value)}
      </div>
      <div className="thai" style={{fontSize:11,color:'var(--muted)',marginTop:2,whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{sub}</div>
    </div>
  );
}

function ChannelItemModal({ requester, guard }){
  const today = localToday();
  const [itemNo, setItemNo] = React.useState('');
  const [locked, setLocked] = React.useState(false);
  const [mode, setMode]     = React.useState('idle');  // idle | new | edit
  const [acc, setAcc] = React.useState('');
  const [createDate, setCreateDate] = React.useState('');
  const [reqr, setReqr] = React.useState(requester||'');   // shown value of Requester (auto)
  const savingRef = React.useRef(false);
  const [orig, setOrig] = React.useState({acc:''});   // baseline for edit-mode change detection
  const [confirmDelete, setConfirmDelete] = React.useState(false);
  const [confirmSave, setConfirmSave] = React.useState(false);
  const [confirmClose, setConfirmClose] = React.useState(false);
  // Permission gate — role must be allowed to manage / delete master sales channel items.
  const role = (()=>{ try { return localStorage.getItem('finx_active_role')||'Viewer'; } catch(_){ return 'Viewer'; } })();
  const perms = (window.loadPermissions && window.loadPermissions()) || {};
  const canManage = (perms[role]||{}).general === true || role==='Admin' || role==='Manager';
  const canDeletePerm = (perms[role]||{}).delete === true || role==='Admin';
  // Normalize a Channel Name: trim + collapse internal whitespace.
  const normName = (s)=> String(s||'').replace(/\s+/g,' ').trim();
  // In Create mode, keep Requester bound to the live Responsible Person.
  React.useEffect(()=>{ if (mode==='new' || mode==='idle') setReqr(requester||''); }, [requester, mode]);
  const fieldsEnabled = mode === 'new' || mode === 'edit';
  // In edit mode, require an actual change to the Channel Name.
  const editChanged = acc.trim()!==orig.acc;
  const canSave   = canManage && fieldsEnabled && acc.trim() !== '' && (mode!=='edit' || editChanged);
  const canDelete = canDeletePerm && mode === 'edit';
  // Report unsaved work to the parent modal's close guard.
  const isDirty = fieldsEnabled && (mode==='new' ? !!acc.trim() : editChanged);
  React.useEffect(()=>{ if (guard){ guard.current.dirty = !!isDirty;
    guard.current.requestClose = ()=>{ setConfirmClose(true); return false; }; } }, [isDirty, guard]);

  const reset = () => { setItemNo(''); setLocked(false); setMode('idle'); setAcc(''); setCreateDate(''); setReqr(requester||''); setOrig({acc:''}); setConfirmDelete(false); setConfirmSave(false); setConfirmClose(false); if (guard) guard.current.dirty=false; };
  const nextItem = () => ensureDescDB().reduce((m,r)=>Math.max(m, Number(r.item)||0), 0) + 1;

  const onAdd = () => {
    if (!canManage){ window.toast('บทบาท '+role+' ไม่มีสิทธิ์สร้าง/แก้ไขช่องทางการขาย', {tone:'warn', title:'ไม่มีสิทธิ์'}); return; }
    const n = nextItem();
    setItemNo(String(n)); setLocked(true); setMode('new');
    setAcc(''); setCreateDate(today); setReqr(requester||''); setOrig({acc:''});
    window.toast('สร้าง Item '+n+' · กรอกรายละเอียดได้เลย', {tone:'info'});
  };

  const recall = () => {
    const n = Number(itemNo);
    if (!itemNo.trim() || isNaN(n)){ window.toast('กรุณากรอกเลขที่ Item', {tone:'warn'}); return; }
    const row = ensureDescDB().find(r => Number(r.item) === n);
    if (!row){ window.toast(`ไม่พบ Item ${itemNo} ในรายการ`, {tone:'warn', title:'ค้นหาไม่พบ'}); return; }
    setAcc(row.channelName||'');
    setCreateDate(row.createDate||today);
    setReqr(row.requester||'');                 // show the original creator from DB
    setOrig({acc:(row.channelName||'').trim()});
    setLocked(true); setMode('edit');
  };

  const commitSave = ()=>{
    if (savingRef.current) return;   // guard against double-click
    savingRef.current = true;
    try {
      const cur = ensureDescDB();
      const idx = cur.findIndex(r => Number(r.item) === Number(itemNo));
      const timeOfEntry = new Date().toTimeString().slice(0,8);   // HH:MM:SS
      const base = { item:Number(itemNo), channelName:normName(acc),
                     createDate, requester:(reqr||'').trim() };
      if (idx >= 0){ cur[idx] = { ...cur[idx], ...base }; }    // UPDATE — keep original timeOfEntry
      else { cur.push({ ...base, timeOfEntry }); }             // INSERT — stamp new timeOfEntry
      saveDescDB(cur);
      window.toast(`บันทึก Item ${itemNo} แล้ว`, {tone:'success', title: idx>=0?'อัพเดทรายการ':'เพิ่มรายการ'});
      reset();
    } finally { savingRef.current = false; }
  };
  const onSaveClick = () => {
    if (!canManage){ window.toast('บทบาท '+role+' ไม่มีสิทธิ์บันทึกช่องทางการขาย', {tone:'warn', title:'ไม่มีสิทธิ์'}); return; }
    if (!fieldsEnabled){ window.toast('กรุณากด Create หรือค้นหา Item ก่อน', {tone:'warn'}); return; }
    const accV = normName(acc);
    if (!accV){ window.toast('กรุณากรอก Channel Name', {tone:'warn', title:'ข้อมูลไม่ครบ'}); return; }
    if (accV.length < 2){ window.toast('Channel Name สั้นเกินไป · กรุณาระบุอย่างน้อย 2 ตัวอักษร', {tone:'warn', title:'ข้อมูลไม่ถูกต้อง'}); return; }
    if (mode==='edit' && !editChanged){ window.toast('ไม่มีการเปลี่ยนแปลง · แก้ไขค่าก่อนจึงจะบันทึกได้', {tone:'warn', title:'ไม่มีการแก้ไข'}); return; }
    // Duplicate guard: same Channel Name on a DIFFERENT item is not allowed.
    const dup = ensureDescDB().find(r => (r.channelName||'').trim().toLowerCase() === accV.toLowerCase()
                             && Number(r.item) !== Number(itemNo));
    if (dup){ window.toast(`Channel Name "${accV}" มีอยู่แล้วที่ Item ${dup.item}`, {tone:'danger', title:'ชื่อซ้ำ'}); return; }
    // Edit mode → confirm before overwriting the existing record (inline overlay, stays on this page).
    if (mode === 'edit'){
      setConfirmSave(true);
      return;
    }
    commitSave();
  };

  const onDeleteClick = () => {
    if (!canDeletePerm){ window.toast('บทบาท '+role+' ไม่มีสิทธิ์ลบช่องทางการขาย', {tone:'warn', title:'ไม่มีสิทธิ์'}); return; }
    setConfirmDelete(true);
  };
  const doDelete = () => {
    saveDescDB(ensureDescDB().filter(r => Number(r.item) !== Number(itemNo)));
    window.toast(`ลบ Item ${itemNo} แล้ว`, {tone:'success', title:'ลบรายการ'});
    reset();
  };

  const txtField = (label, val, setter, ph) => (
    <div className="field">
      <label>{label}</label>
      <input className="input thai" value={val} disabled={!fieldsEnabled}
        onChange={e=>setter(e.target.value)} placeholder={ph}
        style={!fieldsEnabled ? {background:'#f1f3f9',color:'#9aa3c2',cursor:'not-allowed'} : {}}/>
    </div>
  );
  const lockedField = (label, val, icon) => (
    <div className="field">
      <label>{label} <span style={{color:'#7b87a8',fontWeight:500,fontSize:11,marginLeft:4}}>(auto)</span></label>
      <div style={{position:'relative'}}>
        <input className="input thai" value={val} readOnly
          style={{background:'linear-gradient(135deg,#eef1f8,#fff)',color:'#1f2a8e',fontWeight:600,paddingRight:34,cursor:'not-allowed'}}/>
        <span style={{position:'absolute',right:12,top:'50%',transform:'translateY(-50%)'}}>{icon}</span>
      </div>
    </div>
  );

  return (
    <div style={{display:'grid',gap:14,position:'relative'}}>
      {confirmClose && (
        <div style={{position:'fixed',inset:0,zIndex:1000,background:'rgba(16,24,40,.45)',display:'grid',placeItems:'center'}}
             onClick={(e)=>{ if(e.target===e.currentTarget) setConfirmClose(false); }}>
          <div style={{background:'#fff',borderRadius:16,boxShadow:'0 24px 60px -16px rgba(16,24,40,.4)',padding:'22px 24px',maxWidth:440,width:'90%'}} onClick={e=>e.stopPropagation()}>
            <div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',marginBottom:14}}>
              <div style={{fontWeight:700,fontSize:15,color:'var(--ink)'}}>Close <span style={{color:'#b91c1c'}}>ปิดหน้านี้ ?</span></div>
              <button onClick={()=>setConfirmClose(false)} style={{background:'none',border:'none',fontSize:20,color:'#9aa3c2',cursor:'pointer',lineHeight:1}}>×</button>
            </div>
            <div style={{fontSize:13,display:'flex',alignItems:'center',gap:12,marginBottom:20,color:'var(--ink-2)'}}><StatusHalo kind="danger"/><span>การเปลี่ยนแปลงยังไม่ถูกบันทึก ต้องการออกจากหน้านี้หรือไม่ ?</span></div>
            <div className="row" style={{gap:8,justifyContent:'flex-end'}}>
              <button className="btn" onClick={()=>setConfirmClose(false)}>Cancel</button>
              <button className="btn danger" onClick={()=>{ if(guard) guard.current.dirty=false; setConfirmClose(false); window.closeModal(); }}>Close without saving</button>
            </div>
          </div>
        </div>
      )}
      {confirmSave && (
        <div style={{position:'fixed',inset:0,zIndex:1000,background:'rgba(16,24,40,.45)',display:'grid',placeItems:'center'}}
             onClick={(e)=>{ if(e.target===e.currentTarget) setConfirmSave(false); }}>
          <div style={{background:'#fff',borderRadius:16,boxShadow:'0 24px 60px -16px rgba(16,24,40,.4)',padding:'22px 24px',maxWidth:440,width:'90%'}} onClick={e=>e.stopPropagation()}>
            <div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',marginBottom:14}}>
              <div style={{fontWeight:700,fontSize:15,color:'var(--ink)'}}>Save <span style={{color:'#0f766e'}}>ยืนยันการบันทึก ?</span></div>
              <button onClick={()=>setConfirmSave(false)} style={{background:'none',border:'none',fontSize:20,color:'#9aa3c2',cursor:'pointer',lineHeight:1}}>×</button>
            </div>
            <div style={{fontSize:13,display:'flex',alignItems:'center',gap:12,marginBottom:20,color:'var(--ink-2)'}}><StatusHalo kind="info"/><span>{`ต้องการบันทึกการเปลี่ยนแปลงแทนที่ข้อมูลเดิมของ `}<b style={{color:'#0f766e'}}>{`Item ${itemNo}`}</b>{` หรือไม่ ?`}</span></div>
            <div className="row" style={{gap:8,justifyContent:'flex-end'}}>
              <button className="btn" onClick={()=>setConfirmSave(false)}>Cancel</button>
              <button className="btn primary" onClick={()=>{ setConfirmSave(false); commitSave(); }}><I.Save size={14}/> Update</button>
            </div>
          </div>
        </div>
      )}
      {confirmDelete && (
        <div style={{position:'fixed',inset:0,zIndex:1000,background:'rgba(16,24,40,.45)',display:'grid',placeItems:'center'}}
             onClick={(e)=>{ if(e.target===e.currentTarget) setConfirmDelete(false); }}>
          <div style={{background:'#fff',borderRadius:16,boxShadow:'0 24px 60px -16px rgba(16,24,40,.4)',padding:'22px 24px',maxWidth:440,width:'90%'}} onClick={e=>e.stopPropagation()}>
            <div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',marginBottom:14}}>
              <div style={{fontWeight:700,fontSize:15,color:'var(--ink)'}}>Delete <span style={{color:'#b91c1c'}}>ช่องทางการขาย ?</span></div>
              <button onClick={()=>setConfirmDelete(false)} style={{background:'none',border:'none',fontSize:20,color:'#9aa3c2',cursor:'pointer',lineHeight:1}}>×</button>
            </div>
            <div style={{fontSize:13,display:'flex',alignItems:'center',gap:12,marginBottom:20,color:'var(--ink-2)'}}><span style={{width:38,height:38,borderRadius:99,flexShrink:0,display:'grid',placeItems:'center',background:'rgba(245,158,11,.14)'}}><span style={{width:26,height:26,borderRadius:99,display:'grid',placeItems:'center',background:'linear-gradient(135deg,#fbbf24,#d97706)',boxShadow:'0 2px 6px -2px rgba(217,119,6,.5)'}}><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18"/><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/></svg></span></span><span>{`คุณต้องการลบช่องทางการขาย `}<b style={{color:'#b91c1c'}}>{`Item ${itemNo}`}</b>{` นี้ใช่หรือไม่ ?`}</span></div>
            <div className="row" style={{gap:8,justifyContent:'flex-end'}}>
              <button className="btn" onClick={()=>setConfirmDelete(false)}>Cancel</button>
              <button className="btn danger" onClick={doDelete}>Confirm</button>
            </div>
          </div>
        </div>
      )}
      {/* Action buttons — top-right */}
      <div className="row" style={{gap:8,justifyContent:'flex-end'}}>
        <button className="btn success" onClick={onAdd} disabled={itemNo.trim()!=='' || !canManage}
          title={!canManage ? 'บทบาท '+role+' ไม่มีสิทธิ์สร้างช่องทางการขาย' : (itemNo.trim()!=='' ? 'มีค่าในช่อง Item อยู่ — ล้างช่อง Item ก่อนจึงจะ Create ได้' : '')}
          style={(itemNo.trim()!=='' || !canManage) ? {opacity:.45,cursor:'not-allowed'} : {}}><I.Plus size={14}/> Create</button>
        <button className="btn danger" onClick={onDeleteClick} disabled={!canDelete}
          style={!canDelete ? {opacity:.45,cursor:'not-allowed'} : {}}><I.Trash size={14}/> Delete</button>
        <button className="btn primary" onClick={onSaveClick} disabled={!canSave}
          style={!canSave ? {opacity:.45,cursor:'not-allowed'} : {}}><I.Save size={14}/> Save</button>
      </div>

      {/* Item · Create date · Requester · Channel Name (single grid → equal columns) */}
      <div style={{display:'grid',gridTemplateColumns:'1fr 1fr 1fr',gap:12,rowGap:16}}>
        <div className="field">
          <label>Item <span className="req">*</span></label>
          <div style={{position:'relative'}}>
            <input className="input mono" value={itemNo} readOnly={locked}
              onChange={e=>setItemNo(e.target.value.replace(/[^\d]/g,''))}
              onKeyDown={e=>{ if(e.key==='Enter'){ e.preventDefault(); recall(); } }}
              placeholder="ค้นหา หรือ กด Create"
              style={{fontWeight:700,letterSpacing:'.04em',paddingRight:locked?34:12,
                background: locked ? 'linear-gradient(135deg,#eef1f8,#fff)' : '#fff',
                color: locked ? '#1f2a8e' : 'var(--ink)',
                cursor: locked ? 'not-allowed' : 'text'}}/>
            {locked && <I.Lock size={14} stroke="#7b87a8"
              style={{position:'absolute',right:12,top:'50%',transform:'translateY(-50%)'}}/>}
          </div>
        </div>
        <div className="field">
          <label>Creation Date <span style={{color:'#7b87a8',fontWeight:500,fontSize:11,marginLeft:4}}>(auto)</span></label>
          <div style={{position:'relative'}}>
            <input className="input mono" value={createDate ? fmtDateFull(createDate) : ''} readOnly placeholder="วว/ดด/ปปปป"
              style={{background:createDate?'linear-gradient(135deg,#eef1f8,#fff)':'#fafbff',color:createDate?'#1f2a8e':'#9aa3c2',fontWeight:600,paddingRight:34,cursor:'not-allowed'}}/>
            <I.Cal size={14} stroke="#7b87a8" style={{position:'absolute',right:12,top:'50%',transform:'translateY(-50%)'}}/>
          </div>
        </div>
        {lockedField('Requester', reqr||'—', <I.Lock size={14} stroke="#7b87a8"/>)}
        {txtField('Channel Name', acc, setAcc, 'เช่น Retail, Online, B2B')}
      </div>
    </div>
  );
}

// Status icon — circular badge with soft halo rings (per mock): kind 'danger' = red X, 'warn' = amber !
function StatusHalo({kind}){
  const c = kind==='update'
    ? {ring:'rgba(39,71,200,.12)',grad:'linear-gradient(135deg,#5b7cfa,#2747c8)',glow:'0 2px 6px -2px rgba(39,71,200,.5)'}
    : kind==='warn'
    ? {ring:'rgba(245,158,11,.14)',grad:'linear-gradient(135deg,#fbbf24,#d97706)',glow:'0 2px 6px -2px rgba(217,119,6,.5)'}
    : kind==='info'
    ? {ring:'rgba(39,71,200,.12)',grad:'linear-gradient(135deg,#5b7cfa,#2747c8)',glow:'0 2px 6px -2px rgba(39,71,200,.5)'}
    : {ring:'rgba(239,68,68,.12)',grad:'linear-gradient(135deg,#f4526e,#dc2640)',glow:'0 2px 6px -2px rgba(220,38,64,.5)'};
  return (
    <span style={{width:38,height:38,borderRadius:99,flexShrink:0,display:'grid',placeItems:'center',background:c.ring}}>
      <span style={{width:26,height:26,borderRadius:99,display:'grid',placeItems:'center',background:c.grad,boxShadow:c.glow}}>
        {kind==='update'
          ? <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M21 2v6h-6"/><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M3 22v-6h6"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/></svg>
          : kind==='warn'
          ? <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="3.2" strokeLinecap="round"><path d="M12 5v9"/><circle cx="12" cy="19" r="1.4" fill="#fff" stroke="none"/></svg>
          : kind==='info'
          ? <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="3.2" strokeLinecap="round"><circle cx="12" cy="7" r="1.4" fill="#fff" stroke="none"/><path d="M12 11v7"/></svg>
          : <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="3.4" strokeLinecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>}
      </span>
    </span>
  );
}

function EditSeForm({ onSubmit }){
  const [val, setVal] = React.useState('');
  const [q, setQ] = React.useState('');
  const [fChannel, setFChannel] = React.useState('');
  const [fPeriod, setFPeriod] = React.useState('');
  const [page, setPage] = React.useState(0);
  const PAGE_SIZE = 5;
  const records = React.useMemo(()=>{
    let db = {};
    try { db = JSON.parse(localStorage.getItem('finx_sales_requests_v1') || '{}'); } catch(_){ db = {}; }
    return Object.entries(db)
      .filter(([,r]) => (r.status||'')!=='Canceled')
      .map(([id,r]) => {
        const total = (r.rows||[]).reduce((s,x)=>s+(Number(x.amount)||0),0);
        const est   = (r.rows||[]).reduce((s,x)=>s+(Number(x.estimate)||0),0);
        // Date range from the imported rows (earliest → latest), e.g. 1-31/07/2026
        const ds = (r.rows||[]).map(x=>x.date).filter(Boolean).map(d=>new Date(d)).filter(d=>!isNaN(d)).sort((a,b)=>a-b);
        let dateRange = r.reqDate||'';
        if (ds.length){
          const lo=ds[0], hi=ds[ds.length-1];
          const p=(n)=>String(n).padStart(2,'0');
          if (ds.length===1 || (lo.getMonth()===hi.getMonth() && lo.getFullYear()===hi.getFullYear())){
            dateRange = (lo.getDate()===hi.getDate())
              ? `${lo.getDate()}/${p(lo.getMonth()+1)}/${lo.getFullYear()}`
              : `${lo.getDate()}-${hi.getDate()}/${p(hi.getMonth()+1)}/${hi.getFullYear()}`;
          } else {
            dateRange = `${p(lo.getDate())}/${p(lo.getMonth()+1)}/${lo.getFullYear()} - ${p(hi.getDate())}/${p(hi.getMonth()+1)}/${hi.getFullYear()}`;
          }
        }
        return { id, company:(r.channel ?? r.company) || '—', date:r.reqDate||'', dateRange, period:((r.fm||'')+' '+(r.fy||'')).trim(),
                 status:r.status||'Draft', total, est, items:(r.rows||[]).length, savedAt:r.savedAt||'' };
      })
      .sort((a,b)=> {
        // Newest first: by savedAt if present, else by request date, else id.
        const ka = a.savedAt || a.date || '', kb = b.savedAt || b.date || '';
        return ka < kb ? 1 : ka > kb ? -1 : 0;
      });
  }, []);
  const channels = [...new Set(records.map(r=>r.company).filter(Boolean))];
  const periods   = [...new Set(records.map(r=>r.period).filter(Boolean))];
  const filtered = records.filter(r =>
    (!fChannel || r.company===fChannel) &&
    (!fPeriod || r.period===fPeriod) &&
    (!q.trim() || r.company.toLowerCase().includes(q.toLowerCase()) || r.period.toLowerCase().includes(q.toLowerCase()) || (r.date||'').includes(q.trim())));
  const pageCount = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
  const curPage = Math.min(page, pageCount-1);
  const paged = filtered.slice(curPage*PAGE_SIZE, curPage*PAGE_SIZE + PAGE_SIZE);
  React.useEffect(()=>{ setPage(0); }, [q, fChannel, fPeriod]);
  const fmtTHB = n => (Number(n)||0).toLocaleString('en-US',{minimumFractionDigits:2,maximumFractionDigits:2});
  const pill = s => s==='Submitted'
    ? {background:'#dcfce7',color:'#15803d',border:'1px solid #bbf7d0'}
    : {background:'#fee2e2',color:'#b91c1c',border:'1px solid #fecaca'};
  return (
    <div style={{display:'grid',gap:12}}>
      <div style={{fontSize:13,color:'var(--ink-2)',lineHeight:1.55}}>
        เลือกรายการที่ต้องการแก้ไข (แสดงเฉพาะรายการที่ยังไม่ถูกยกเลิก)
      </div>
      <div style={{display:'flex',gap:8,alignItems:'center'}}>
        <div className="search" style={{flex:'1 1 auto',minWidth:120}}>
          <I.Search size={14} stroke="#9aa3c2"/>
          <input placeholder="ค้นหา Channel, Date หรือ Period" value={q} onChange={e=>setQ(e.target.value)}/>
        </div>
        <select className="select" value={fChannel} onChange={e=>setFChannel(e.target.value)} style={{fontSize:12.5,paddingRight:52,flex:'0 0 auto',width:'auto',minWidth:170}}>
          <option value="">All Channel</option>
          {channels.map(c=><option key={c} value={c}>{c}</option>)}
        </select>
        <select className="select" value={fPeriod} onChange={e=>setFPeriod(e.target.value)} style={{fontSize:12.5,paddingRight:44,flex:'0 0 auto',width:'auto',minWidth:125}}>
          <option value="">All Period</option>
          {periods.map(p=><option key={p} value={p}>{p}</option>)}
        </select>
      </div>
      <div className="edit-rec-list" style={{maxHeight:380,overflowY:'auto',display:'grid',gap:9,padding:12,border:'1px solid var(--line)',borderRadius:14,background:'linear-gradient(180deg,#f6f8fc,#eef1f8)',alignContent:'start'}}>
        {paged.length===0 && (
          <div style={{padding:28,textAlign:'center',color:'var(--muted)',fontSize:13,border:'1px dashed var(--line)',borderRadius:12}}>ไม่พบรายการที่ตรงเงื่อนไข</div>
        )}
        {paged.map(r => {
          const on = val===r.id;
          const sub = r.status==='Submitted';
          const c = sub ? {grad:'linear-gradient(135deg,#22c55e,#15803d)',bar:'linear-gradient(180deg,#22c55e,#15803d)',glow:'0 3px 8px -4px rgba(34,197,94,.35)',cardGrad:'linear-gradient(135deg,#f0fdf4 0%,#fff 55%)',shadow:'0 4px 12px -8px rgba(34,197,94,.25)',border:'1.5px solid #15803d'}
                        : {grad:'linear-gradient(135deg,#f87171,#b91c1c)',bar:'linear-gradient(180deg,#f87171,#b91c1c)',glow:'0 3px 8px -4px rgba(239,68,68,.35)',cardGrad:'linear-gradient(135deg,#fef2f2 0%,#fff 55%)',shadow:'0 4px 12px -8px rgba(239,68,68,.25)',border:'1.5px solid #b91c1c'};
          return (
            <div key={r.id} onClick={()=>setVal(on ? '' : r.id)} className={'edit-rec-row'+(on?' on':'')}
              style={{position:'relative',display:'flex',justifyContent:'space-between',alignItems:'center',gap:14,padding:'13px 16px 13px 20px',
                cursor:'pointer',borderRadius:13,overflow:'hidden',
                border: on ? c.border : '1px solid #e5e9f2',
                background: on ? c.cardGrad : '#fff',
                boxShadow: on ? c.shadow : '0 1px 2px rgba(15,23,42,.05)',
                transition:'transform .16s ease, box-shadow .16s ease, border-color .16s ease'}}>
              <span style={{position:'absolute',left:0,top:0,bottom:0,width:4,borderRadius:'4px 0 0 4px',
                background: c.bar}}></span>
              <div style={{width:38,height:38,borderRadius:11,flexShrink:0,display:'grid',placeItems:'center',
                background: on ? c.grad : 'linear-gradient(135deg,#eef1f8,#e2e8f4)',
                color: on ? '#fff' : '#5a6a94', fontWeight:800, fontSize:14, letterSpacing:'.02em',
                boxShadow: on ? c.glow : 'none', transition:'all .16s ease'}}>
                {({'Retail':'RT','Online':'ON','B2B':'B2'}[r.company] || (r.company||'—').replace(/\s/g,'').slice(0,2).toUpperCase())}
              </div>
              <div style={{minWidth:0,flex:1}}>
                <div className="row" style={{gap:8,alignItems:'center'}}>
                  <span className="thai" style={{fontWeight:700,fontSize:13.5,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{r.company}</span>
                  <span style={{...pill(r.status),fontSize:9.5,fontWeight:700,padding:'2.5px 9px',borderRadius:99,flexShrink:0,display:'inline-flex',alignItems:'center',gap:4,textTransform:'uppercase',letterSpacing:'.04em'}}>
                    <span style={{width:5,height:5,borderRadius:99,background:'currentColor'}}></span>{r.status}
                  </span>
                </div>
                <div style={{fontSize:11.5,color:'var(--muted)',marginTop:3,display:'flex',gap:6,alignItems:'center'}}>
                  <span className="mono">{r.dateRange||r.date}</span><span style={{opacity:.4}}>·</span><span>{r.period}</span><span style={{opacity:.4}}>·</span><span>{r.items} items</span>
                </div>
              </div>
              <div style={{textAlign:'right',flexShrink:0}}>
                <div className="mono" style={{fontWeight:800,fontSize:13.5,color:'#1f2a8e'}}>Act. {fmtTHB(r.total)} <span style={{fontSize:10,opacity:.6}}>THB</span></div>
                <div className="mono" style={{fontSize:10.5,color:'var(--muted)',marginTop:2}}>Est. {fmtTHB(r.est)} <span style={{fontSize:9,opacity:.7}}>THB</span></div>
              </div>
              <div style={{width:20,height:20,borderRadius:99,flexShrink:0,display:'grid',placeItems:'center',transition:'all .16s ease',
                border: on ? 'none' : '1.5px solid #d4dbe8',
                background: on ? c.grad : '#fff'}}>
                {on && <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="3.4" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6L9 17l-5-5"/></svg>}
              </div>
            </div>
          );
        })}
      </div>
      <div className="row" style={{justifyContent:'space-between',alignItems:'center'}}>
        <span style={{fontSize:12.5,color:'var(--muted)'}}>ทั้งหมด {filtered.length} รายการ</span>
        {pageCount > 1 && (
          <div className="row" style={{gap:8,alignItems:'center'}}>
            <button className="btn" disabled={curPage===0} onClick={()=>setPage(curPage-1)} style={{padding:'5px 12px',fontSize:12}}>‹ Previous</button>
            <span style={{fontSize:12.5,color:'var(--muted)'}}>หน้า {curPage+1} / {pageCount}</span>
            <button className="btn" disabled={curPage>=pageCount-1} onClick={()=>setPage(curPage+1)} style={{padding:'5px 12px',fontSize:12}}>Next ›</button>
          </div>
        )}
      </div>
      <div className="row" style={{gap:8,justifyContent:'flex-end'}}>
        <button className="btn" onClick={()=>window.closeModal()}>Cancel</button>
        <button className="btn primary" disabled={!val}
          onClick={()=>{ onSubmit(val); window.closeModal(); }}>
          <I.Edit size={14}/> Load for editing
        </button>
      </div>
    </div>
  );
}

function SalesPreview({ form, total, totalTarget, totalActual, itemCount }){
  const rows = (form.rows||[]).slice().sort((a,b)=>{
    const da=a.date?new Date(a.date):0, db2=b.date?new Date(b.date):0;
    return da-db2;
  });
  // Date range from the imported rows (earliest → latest).
  const dates = (form.rows||[]).map(r=>r.date).filter(Boolean).map(d=>new Date(d)).filter(d=>!isNaN(d)).sort((a,b)=>a-b);
  const dateRange = dates.length
    ? (fmtDateFull(dates[0]) + (dates.length>1 ? ' - ' + fmtDateFull(dates[dates.length-1]) : ''))
    : '—';
  const meta = [
    ['Date', dateRange],
    ['Company name', form.companyName],
    ['Channel name', form.channel],
    ['Period', form.fm+' '+form.fy],
  ];
  return (
    <div>
      <div style={{
        background:'linear-gradient(135deg,#1f2a8e,#2e3bbf)',color:'#fff',
        padding:18,borderRadius:14,marginBottom:18,display:'flex',justifyContent:'space-between'
      }}>
        <div style={{display:'flex',gap:30,alignItems:'center'}}>
          <div>
            <div style={{fontSize:11,letterSpacing:'.1em',textTransform:'uppercase',opacity:.7}}>Target</div>
            <div className="mono" style={{fontSize:28,fontWeight:800,marginTop:4}}>{fmt(totalTarget)} <span style={{fontSize:13,opacity:.7}}>THB</span></div>
          </div>
          <div style={{width:1,alignSelf:'stretch',background:'rgba(255,255,255,.22)'}}/>
          <div>
            <div style={{fontSize:11,letterSpacing:'.1em',textTransform:'uppercase',opacity:.7}}>Actual</div>
            <div className="mono" style={{fontSize:28,fontWeight:800,marginTop:4}}>{fmt(totalActual)} <span style={{fontSize:13,opacity:.7}}>THB</span></div>
          </div>
        </div>
        <div style={{textAlign:'right'}}>
          <div style={{fontSize:11,opacity:.7}}>Rows</div>
          <div className="mono" style={{fontSize:22,fontWeight:800}}>{itemCount}</div>
        </div>
      </div>
      <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:6,marginBottom:14}}>
        {meta.map(([k,v],i)=>(
          <div key={i} style={{display:'flex',justifyContent:'space-between',padding:'8px 12px',background:i%2?'#f7f8fc':'transparent',borderRadius:6,fontSize:13}}>
            <span style={{color:'var(--muted)'}}>{k}</span>
            <span className="thai" style={{fontWeight:600}}>{v}</span>
          </div>
        ))}
      </div>
      <div style={{fontWeight:700,fontSize:13,marginBottom:6}}>Sales breakdown</div>
      <div style={{maxHeight:240,overflow:'auto',border:'1px solid var(--line)',borderRadius:8}}>
        {rows.length===0 && <div style={{padding:18,textAlign:'center',color:'var(--muted)',fontSize:13}}>No sales imported</div>}
        {rows.length>0 && (
          <div style={{display:'flex',justifyContent:'space-between',padding:'8px 12px',gap:10,fontSize:11,fontWeight:700,letterSpacing:'.04em',textTransform:'uppercase',color:'var(--muted)',background:'#eef1f8',position:'sticky',top:0}}>
            <span style={{width:96,textAlign:'center',flex:'none'}}>Date</span>
            <span style={{minWidth:0,flex:1,textAlign:'center'}}>Sales Channel</span>
            <span style={{width:110,textAlign:'right',flex:'none'}}>Target</span>
            <span style={{width:110,textAlign:'right',flex:'none'}}>Actual</span>
            <span style={{width:110,textAlign:'right',flex:'none'}}>Variance</span>
          </div>
        )}
        {rows.map((r,i)=>{
          const est=Number(r.estimate)||0, act=Number(r.amount)||0, variance=act-est;
          return (
          <div key={i} style={{display:'flex',justifyContent:'space-between',padding:'8px 12px',background:i%2?'#f7f8fc':'#fff',fontSize:13,gap:10}}>
            <span className="mono" style={{width:96,textAlign:'center',flex:'none'}}>{fmtDateFull(r.date)}</span>
            <span className="thai" style={{minWidth:0,flex:1,textAlign:'center',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{r.channel||r.expenseGroup||'—'}</span>
            <span className="mono" style={{width:110,textAlign:'right',flex:'none',fontWeight:700,color:est===0?'#c3c9da':'#0f766e'}}>{fmt(est)}</span>
            <span className="mono" style={{width:110,textAlign:'right',flex:'none',fontWeight:700,color:act===0?'#c3c9da':'#1f2a8e'}}>{fmt(act)}</span>
            <span className="mono" style={{width:110,textAlign:'right',flex:'none',fontWeight:700,color:variance>0?'#059669':variance<0?'#dc2626':'#c3c9da'}}>{(variance>0?'+':'')+fmt(variance)}</span>
          </div>
          );
        })}
      </div>
    </div>
  );
}

window.SalesPerformanceView = SalesPerformanceView;

})();
