// General Information — expense entry form with import + review workflow.
//
//  Storage model (localStorage):
//    finx_expense_requests_v1 : { _id: {record...} }  — Draft/Submitted records = "Database"
//    (record key = internal _id from newRecordId(); no external running number)
function expenseDB(){ try { return JSON.parse(localStorage.getItem('finx_expense_requests_v1') || '{}'); } catch(_){ return {}; } }

// Persist the request DB safely. Returns true on success; on failure (quota full /
// storage blocked) shows a toast and returns false so callers can abort cleanly.
function saveExpenseDB(db){
  try { localStorage.setItem('finx_expense_requests_v1', JSON.stringify(db));
        window.dispatchEvent(new CustomEvent('finx:status-changed'));   // keep the Approve badge in sync
        return true; }
  catch(e){
    try { window.toast('บันทึกไม่สำเร็จ — พื้นที่จัดเก็บเต็มหรือถูกบล็อก กรุณาลบรายการเก่าหรือ Export ออกก่อน', {tone:'danger', title:'Save failed', ttl:5000}); } catch(_){}
    return false;
  }
}
// ── 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 ใช้การบีบอัดที่ไม่รองรับ');
}
// Read ZIP entries via the Central Directory (authoritative), then resolve each
// entry's real data by re-reading its Local File Header. This is robust against
// data descriptors (flag bit 3 → sizes are 0 in the local header) and any extra
// fields, which the old "walk local headers from offset 0" approach broke on.
function findZipEntries(buf){
  const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
  const dec = new TextDecoder();
  const entries = {};
  // 1) Locate End Of Central Directory record (scan backwards for its signature).
  let eocd = -1;
  for (let i = buf.length - 22; i >= 0; i--){
    if (dv.getUint32(i, true) === 0x06054b50){ eocd = i; break; }
  }
  if (eocd < 0){
    // Fallback: no EOCD found — walk local headers contiguously (best effort).
    return walkLocalHeaders(buf, dv, dec);
  }
  let cdOffset = dv.getUint32(eocd + 16, true);
  const cdCount = dv.getUint16(eocd + 10, true);
  // 2) Iterate Central Directory File Header records.
  let p = cdOffset;
  for (let n = 0; n < cdCount && p + 46 <= buf.length; n++){
    if (dv.getUint32(p, true) !== 0x02014b50) break;
    const method   = dv.getUint16(p + 10, true);
    let   compSize = dv.getUint32(p + 20, true);
    const nameLen  = dv.getUint16(p + 28, true);
    const extraLen = dv.getUint16(p + 30, true);
    const commLen  = dv.getUint16(p + 32, true);
    const localOff = dv.getUint32(p + 42, true);
    const name = dec.decode(buf.subarray(p + 46, p + 46 + nameLen));
    // Resolve data start from the LOCAL header (its own name/extra lengths).
    if (dv.getUint32(localOff, true) === 0x04034b50){
      const lNameLen  = dv.getUint16(localOff + 26, true);
      const lExtraLen = dv.getUint16(localOff + 28, true);
      const dataStart = localOff + 30 + lNameLen + lExtraLen;
      entries[name] = { method, bytes: buf.subarray(dataStart, dataStart + compSize) };
    }
    p += 46 + nameLen + extraLen + commLen;
  }
  return Object.keys(entries).length ? entries : walkLocalHeaders(buf, dv, dec);
}
// Legacy contiguous walk — only used when the Central Directory is unreadable.
function walkLocalHeaders(buf, dv, dec){
  const entries = {};
  let i = 0;
  while (i + 4 <= buf.length){
    if (dv.getUint32(i, true) !== 0x04034b50) break;
    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 = dec.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 (Item & Report To removed): A=Date, B=Expense Type, C=Description, D=Estimate, E=Actual, F=Accounts Payable
  const num = (s)=> parseFloat(String(s||'').replace(/[,\s"]/g,''));
  const out = [];
  rows.forEach((c,idx)=>{
    const rawDate      = (c.A||'').toString().trim();
    const expenseGroup = (c.B||'').trim();   // Expense Type — validated against Master accountName
    const accountName  = (c.C||'').trim();   // Description (free text, bound to accountName)
    const estimate     = num(c.D);
    const amt          = num(c.E);
    const payable      = (c.F||'').trim();
    // Skip header row & fully-empty rows
    if (idx===0 && isNaN(amt) && isNaN(estimate)) return;
    if (expenseGroup && (!isNaN(amt) || !isNaN(estimate))){
      out.push({ date: normImportDate(rawDate), expenseGroup, accountName,
        estimate: isNaN(estimate)?0:estimate,
        amount: isNaN(amt)?0:amt,
        rawEstimate: (c.D||'').toString(), rawActual: (c.E||'').toString(),
        payable });
    }
  });
  const header = rows[0] ? ['A','B','C','D','E','F'].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','Expense Type','Description','Estimate','Actual','Accounts Payable'];
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 '';
  if (/^\d+(\.\d+)?$/.test(s)){            // Excel serial
    const serial = parseFloat(s);
    const d = new Date(Date.UTC(1899,11,30) + Math.round(serial)*86400000);
    return `${d.getUTCFullYear()}-${String(d.getUTCMonth()+1).padStart(2,'0')}-${String(d.getUTCDate()).padStart(2,'0')}`;
  }
  const m = s.match(/^(\d{1,2})[\/\-.](\d{1,2})[\/\-.](\d{2,4})$/);   // dd/mm/yy(yy)
  if (m){
    let [_,dd,mm,yy] = m;
    let y = parseInt(yy,10); if (y<100) y += 2000;
    return `${y}-${String(parseInt(mm,10)).padStart(2,'0')}-${String(parseInt(dd,10)).padStart(2,'0')}`;
  }
  const d = new Date(s);
  if (!isNaN(d)) return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
  return '';
}

// Master accountName catalog (categories). Seeds the Expenses Description DB and
// serves as the valid set for the Import file's "Expense Type" column.
const EXPENSE_TEMPLATE = [
  ['ค่าเช่า - สาขาเก่า','ค่าเช่าและพื้นที่','Fixed Cost'],
  ['ค่าเช่า - สาขาใหม่','ค่าเช่าและพื้นที่','Fixed Cost'],
  ['ค่าก่อสร้าง - Renovate','ค่าใช้จ่ายลงทุน (CAPEX)','Fixed Cost'],
  ['ค่าก่อสร้าง - Relocation','ค่าใช้จ่ายลงทุน (CAPEX)','Fixed Cost'],
  ['ค่าก่อสร้าง - สาขาใหม่','ค่าใช้จ่ายลงทุน (CAPEX)','Fixed Cost'],
  ['ค่าก่อสร้าง - อื่นๆ','ค่าใช้จ่ายลงทุน (CAPEX)','Fixed Cost'],
  ['ค่าซ่อมแซม-สาขา','ค่าซ่อมบำรุง','Variable Cost'],
  ['ค่าเครื่องจักร และค่าผ่อน','ค่าใช้จ่ายเครื่องจักร/อุปกรณ์','Fixed Cost'],
  ['ค่ารถยนต์ ค่าผ่อน ประกัน','ค่าขนส่งและยานพาหนะ','Fixed Cost'],
  ['เดินทางปฏิบัติงาน','ค่าใช้จ่ายในการดำเนินงาน','Variable Cost'],
  ['สินทรัพย์','ค่าใช้จ่ายลงทุน (CAPEX)','Fixed Cost'],
  ['ค่าสินค้า - ทั่วไป','ต้นทุนขาย (COGS)','Variable Cost'],
  ['ค่าสินค้า - นำเข้า','ต้นทุนขาย (COGS)','Variable Cost'],
  ['ค่าสินค้า JS - OEM','ต้นทุนขาย (COGS)','Variable Cost'],
  ['ขนส่ง','ค่าขนส่งและยานพาหนะ','Variable Cost'],
  ['บัตรน้ำมัน','ค่าขนส่งและยานพาหนะ','Variable Cost'],
  ['Supply Use','ค่าใช้จ่ายในการดำเนินงาน','Variable Cost'],
  ['Online','ค่าการตลาด / ขาย','Variable Cost'],
  ['เงินยืม','รายการปรับปรุง/อื่นๆ','Variable Cost'],
  ['Project','ค่าใช้จ่ายในการดำเนินงาน','Variable Cost'],
  ['ค่าซ่อมแซม-คลัง','ค่าซ่อมบำรุง','Variable Cost'],
  ['ปอศ. / มอก.','ค่าธรรมเนียม/ใบอนุญาต','Fixed Cost'],
  ['ค่าเช่า - อุปกรณ์ดำเนินงาน','ค่าเช่าและพื้นที่','Fixed Cost'],
  ['ค่าระบบ','ค่าใช้จ่ายในการดำเนินงาน','Fixed Cost'],
  ['ค่าสันทนาการ','ค่าสวัสดิการ/อื่นๆ','Variable Cost'],
  ['ค่าสาธารณูปโภค','ค่าใช้จ่ายในการดำเนินงาน','Variable Cost'],
  ['ค่าภาษี','ค่าใช้จ่ายภาษี','Fixed Cost'],
  ['ค่าใช้จ่ายอื่นๆ - สาขา','ค่าใช้จ่ายในการดำเนินงาน','Variable Cost'],
  ['ค่าใช้จ่ายอื่นๆ - คลัง','ค่าใช้จ่ายในการดำเนินงาน','Variable Cost'],
  ['รับเงินคืน','รายการปรับปรุง/อื่นๆ','Variable Cost'],
  ['ฝึกอบรม','ค่าใช้จ่ายบุคลากร','Variable Cost'],
  ['ค่าวัตถุดิบ','ต้นทุนขาย (COGS)','Variable Cost'],
  ['ค่าจ้างผลิต','ต้นทุนขาย (COGS)','Variable Cost'],
  ['เจ้าหนี้ - เช็คจ่าย','ต้นทุนขาย (COGS)','Variable Cost'],
  ['ดอกเบี้ย','ค่าใช้จ่ายอื่น ๆ','Fixed Cost'],
];
const EXPENSE_CATS = EXPENSE_TEMPLATE.map(r=>r[0]);
// Lookup by accountName → { expenseGroup (category), costBehavior }.
// Populated from the master template so imported rows can be enriched (see applyImport).
const CAT_META = {};
EXPENSE_TEMPLATE.forEach(([accountName, expenseGroup, costBehavior]) => {
  CAT_META[accountName] = { expenseGroup, costBehavior };
});

// User-added Expense Group options (localStorage). Standard groups (from the
// master template / DB) can't be deleted — only these custom ones can.
const CUSTOM_GROUPS_KEY = 'finx_expense_groups_custom';
const REMOVED_GROUPS_KEY = 'finx_expense_groups_removed';
function loadCustomGroups(){ try { return JSON.parse(localStorage.getItem(CUSTOM_GROUPS_KEY)||'[]'); } catch(_){ return []; } }
function saveCustomGroups(list){ try { localStorage.setItem(CUSTOM_GROUPS_KEY, JSON.stringify(list)); } catch(_){} }
function loadRemovedGroups(){ try { return JSON.parse(localStorage.getItem(REMOVED_GROUPS_KEY)||'[]'); } catch(_){ return []; } }
function saveRemovedGroups(list){ try { localStorage.setItem(REMOVED_GROUPS_KEY, JSON.stringify(list)); } catch(_){} }
function baseGroups(){
  const s = new Set(EXPENSE_TEMPLATE.map(r=>r[1]));
  ensureDescDB().forEach(r=>{ if((r.expenseGroup||'').trim()) s.add(r.expenseGroup.trim()); });
  return s;
}
// Final selectable set: sorted base (template ∪ DB), then custom groups in the
// order they were added — new groups append after the last item. Minus removed.
function computeGroups(){
  const removed = new Set(loadRemovedGroups());
  const base = [...baseGroups()].filter(g=>!removed.has(g)).sort();
  const baseSet = new Set(base);
  const custom = loadCustomGroups().filter(g=>g && !removed.has(g) && !baseSet.has(g));
  return [...base, ...custom];
}

// ── Expenses Description master catalog (localStorage: finx_expense_desc) ──
// Bumped to re-seed with the 35-item master accountName list.
const DESC_SEED_V = '7';
function descDB(){ try { return JSON.parse(localStorage.getItem('finx_expense_desc')||'null'); } catch(_){ return null; } }
function saveDescDB(arr){ localStorage.setItem('finx_expense_desc', JSON.stringify(arr)); }
function ensureDescDB(){
  let db = descDB();
  const v = localStorage.getItem('finx_expense_desc_v');
  if (!Array.isArray(db) || v !== DESC_SEED_V){
    db = EXPENSE_TEMPLATE.map((r,i)=>({
      item:i+1, accountName:r[0], expenseGroup:r[1], costBehavior:r[2],
      createDate:'2026-06-01', requester:'thongchai.hoh', timeOfEntry:'09:00:00'
    }));
    saveDescDB(db);
    localStorage.setItem('finx_expense_desc_v', DESC_SEED_V);
  }
  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 initialSelling = () => ({
  _id: '',
  reqDate: '',
  currency: '',
  fy: '',
  fm: '',
  company: '',
  rows: [],                 // imported expense rows: {accountName, expenseGroup, costBehavior, amount, entryDate}
});

// 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 ExpenseAmountCell({ 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: Estimate 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 GeneralInformationView({ onNav, title='General Information', subtitle='บันทึกข้อมูลค่าใช้จ่าย', detailLabel='Expenses Details', steps=['General','Expenses','Review'], generalPaneTitle='General Information' }){
  const clock = useSystemClock();
  const [form, setForm] = useState(()=> initialSelling());
  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 [expensesUnlocked, setExpensesUnlocked] = 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_expense_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 false; }   // fail-closed (consistent with canDelete)
  };
  // 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 expensesEditable = expensesUnlocked && !reviewUnlocked;   // locked (gray) once Next: Review is pressed; re-editing unlocks
  const canSubmitExpense = expensesUnlocked && reviewUnlocked;

  // No seed data — the system starts empty. Submitted requests are stored in
  // localStorage (finx_expense_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'});   // Expenses Details 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 Expenses Details,
  // so the imported rows can never be out of sync with the general-information header.
  const TRIGGER_FIELDS = ['reqDate','company','fm','fy'];
  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}}>มีการแก้ไข General Information ข้อมูลใน Expenses Details จะถูกรีเซ็ต โปรดนำเข้าข้อมูลใหม่อีกครั้ง</span>,
        tone:'danger', okLabel:'Confirm & Reset', cancelLabel:'Cancel',
        onOk: ()=>{
          markDirty(true);
          setForm(f => ({...f, [k]:v, rows:[]}));
          window.toast('รีเซ็ต Expenses Details แล้ว · กรุณา Import ข้อมูลใหม่', {tone:'warn', title:'Expenses Details 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_expense_requests_v1') || '{}');
    if (saved[q]){
      const rec = saved[q];
      setForm({ ...initialSelling(), ...rec, rows: rec.rows || [] });
      setLocked(true);
      setEditingId(q);           // treat search-loaded record as an edit (same as Edit path)
      setExpensesUnlocked(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 unique row id _rid).
  const updateRowAmount = (rid, val) => {
    setForm(f => ({...f, rows: (f.rows||[]).map(r => r._rid===rid ? {...r, amount: val} : r)}));
    markDirty(true);
  };
  // Edit an imported row's Estimate inline (matched by unique row id _rid).
  const updateRowEstimate = (rid, val) => {
    setForm(f => ({...f, rows: (f.rows||[]).map(r => r._rid===rid ? {...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.accountName||row.expenseGroup||'—'}</b> ออกจาก Expenses Details หรือไม่ ?</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 totalEstimate = (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 expenseFilteredRows = (form.rows||[]).filter(r =>
    !search ||
    (r.expenseGroup||'').toLowerCase().includes(search.toLowerCase()) ||
    (r.accountName||'').toLowerCase().includes(search.toLowerCase()) ||
    (r.payable||'').toLowerCase().includes(search.toLowerCase())
  );
  // Optional column sort (click a sortable header)
  const expenseSortedRows = React.useMemo(()=>{
    if (!sort.key) return expenseFilteredRows;
    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 [...expenseFilteredRows].sort((a,b)=>{
      const av=val(a), bv=val(b);
      if (typeof av==='number') return (av-bv)*dir;
      return av.localeCompare(bv,'th')*dir;
    });
  }, [expenseFilteredRows, sort]);
  // Submitted status — green if the saved record is Submitted, else pending (red)
  const isSubmittedRecord = (()=>{
    if (!form._id) return false;
    const rec = expenseDB()[form._id];
    return !!(rec && rec.status==='Submitted');
  })();
  // Row status for the Expenses Details table: Submitted / Draft (saved as draft) / Pending (in progress, unsaved)
  const rowStatus = (()=>{
    if (!form._id) return 'Pending';
    const rec = expenseDB()[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 Expenses Details 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 Expenses Details.
  const submittedLock = isSubmittedRecord && !!editingId;

  // General Information completeness — all fields must have a value
  const GENERAL_FIELDS = ['reqDate','currency','fy','fm','company'];
  const missingFields = GENERAL_FIELDS.filter(k => !form[k] || String(form[k]).trim()==='');
  const isGeneralComplete = missingFields.length === 0;
  const fieldLabels = {
    reqDate:'Request Date', currency:'Currency',
    fy:'Fiscal Year', fm:'Fiscal Month', company:'Company 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 = EXPENSE_CATS.filter(c =>
    !search || c.toLowerCase().includes(search.toLowerCase())
  );

  const onPreview = () => {
    // Cannot preview until an entry is started (Add Item) AND General Information is complete.
    if (!form._id){
      window.toast('กรุณากด Add Item เพื่อเริ่มรายการก่อนดู Preview', {tone:'warn', title:'ยังไม่ได้ Add Item'});
      return;
    }
    if (!isGeneralComplete){
      window.toast('กรุณากรอกข้อมูลใน General Information ให้ครบทุกช่องก่อนดู Preview', {tone:'warn', title:'ข้อมูลไม่ครบ'});
      return;
    }
    window.openModal({
      title:'Preview', size:'lg',
      message: <SellingPreview form={form} total={total} totalEstimate={totalEstimate} 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_expense_requests_v1') || '{}');
        const rec = saved[id];
        if (!rec){ window.toast('ไม่พบรายการในระบบ', {tone:'warn', title:'ค้นหาไม่พบ'}); return; }
        // Approved records are locked — they must be reopened from the Approve menu first.
        if ((rec.status||'')==='Approved'){
          window.toast('รายการนี้อนุมัติแล้ว จึงถูกล็อกไม่ให้แก้ไข — กรุณา Reopen ที่เมนู Approve ก่อน', {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;
        }
        // Step 1: load data + remember current_id
        setForm({ ...initialSelling(), ...rec, _id:id, rows: rec.rows || [] });
        setLocked(true);
        setEditingId(id);
        setExpensesUnlocked(true);
        setReviewUnlocked(false);  // editing an existing record — keep editable until Next: Review
        setReqSearch('');
        markDirty(false);
        window.logActivity('Opened for editing ('+(rec.status||'Draft')+') · '+(rec.company||'')+' · General Information', '');
        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+' รายการใน Expenses Details"'):''} ข้อมูลที่ยังไม่บันทึกจะหายไป ยืนยันการออกหรือไม่ ?</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 (total <= 0){ window.toast('Enter at least one expense before saving', {tone:'warn'}); return; }
    if (!form._id){ window.toast('กรุณากด Add Item เพื่อเริ่มรายการก่อน', {tone:'warn'}); return; }
    const targetNo = editingId || form._id;
    const existing = expenseDB()[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 = expenseDB();
        const cur = db[targetNo] || {};
        db[targetNo] = { ...cur, ...form, _id:targetNo, rows:cleanRows, status:'Draft', savedAt:new Date().toISOString() };
        if (!saveExpenseDB(db)) return;   // abort (form kept) if storage write failed
        doReset();
        window.logActivity((isUpdate?'Updated draft':'Saved draft')+' · General Information', '');
        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 Estimate : <b style={{color:'#111827'}}>{fmt(totalEstimate)} 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(initialSelling());
    setLocked(false);
    setExpensesUnlocked(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 = expenseDB();
          const rec = db[editingId];
          if (!rec){ window.toast('ไม่พบรายการใน Database', {tone:'danger'}); return; }
          window.logDeleteAudit({
            recordId: editingId,
            company: rec.company||'',
            period: [rec.fm, rec.fy].filter(Boolean).join(' '),
            status: rec.status||'',
            rows: (rec.rows||[]).map(r => ({
              date: r.date||'', expenseGroup: r.expenseGroup||'',
              accountName: r.accountName||'', estimate: Number(r.estimate)||0,
              amount: Number(r.amount)||0, payable: r.payable||''
            }))
          });
          delete db[editingId];
          if (!saveExpenseDB(db)) return;
          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 (total <= 0){ window.toast('Enter at least one expense 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('พบค่า Estimate หรือ Actual ติดลบ — ค่าต้องเป็น 0 หรือมากกว่า', {tone:'danger', title:'ตรวจสอบข้อมูล'}); return; }
    const missingPayable = rows.filter(r => (Number(r.amount)||0) > 0 && !(r.payable||'').trim()).length;
    if (missingPayable){ window.toast('พบ '+missingPayable+' แถวที่ไม่มี Accounts Payable — ต้องกรอกให้ครบ', {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 && (expenseDB()[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)'}}>
                {isUpdate
                  ? <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" 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>
                  : <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 Estimate : <b style={{color:'#111827'}}>{fmt(totalEstimate)} THB</b><br/>Total Actual : <b style={{color:'#111827'}}>{fmt(totalActual)} THB</b></span>
          </span>,
      okLabel: isUpdate ? 'Update' : 'Submit', tone:'primary',
      onOk: ()=>{
        if (savingRef.current) return;   // guard against double-click Submit
        savingRef.current = true;
        window.showLoading('กำลังบันทึกหรือแก้ไขรายการ…');
        setTimeout(()=>{ try {
        // Step 2 backend guard: persist rows that carry a real value in Estimate 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 = expenseDB();
        db[targetNo] = { ...payload, _id:targetNo, status:'Submitted', submittedBy: window.CURRENT_USER, savedAt:new Date().toISOString(), submittedAt:new Date().toISOString() };
        if (!saveExpenseDB(db)) return;
        doReset();
        window.logActivity(isUpdate?'Updated · General Information':'Submitted · General Information', '');
        window.finxNotifyMe && window.finxNotifyMe({ t:'info', view:'general',
          title:(isUpdate?'อัพเดทคำขอ ':'ส่งคำขอ ')+targetNo,
          msg:'General Information · '+(payload.company||'—')+' · รอการอนุมัติ' });
        window.toast((isUpdate?'อัพเดทรายการสำเร็จ':'บันทึกรายการสำเร็จ')+'  · ฟอร์มถูกล้างพร้อมสร้างรายการใหม่',
                     {tone:'success', title: isUpdate?'Updated & saved':'Submitted & saved', ttl:3800});
        } finally { window.hideLoading(); savingRef.current = false; } }, 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>ล้างค่าทั้งหมดกลับเป็นค่าเริ่มต้น (รวมถึง Expenses Details)</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({
        ...initialSelling(),
        _id: newRecordId(),
        reqDate:  localToday(),
        currency: 'THB',                    // locked to THB
        fy:       String(now.getFullYear()),
        fm:       MONTHS[now.getMonth()],
        company:  '',                       // no default — user must choose
      });
      setLocked(true);
      setReqSearch('');
      setExpensesUnlocked(false);
      setReviewUnlocked(false);
      setStep(1);
      markDirty(false);
      window.logActivity('Add Item · General Information', '');
      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 (!expensesEditable){ window.toast('ฟอร์มถูกล็อก (ผ่านขั้น Review แล้ว) · กด "Edit" ที่ Expenses Details เพื่อปลดล็อกก่อน', {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> ใน Expenses Details หรือไม่ ?</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 Expenses Description popup directly (disabled once a file is imported).
  const onNewExpenseItem = () => {
    ensureDescDB();
    const guard = { current: { dirty:false } };   // ExpenseItemModal reports unsaved work here
    window.openModal({
      title:'Expenses Description', size:'lg',
      message: <ExpenseItemModal 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 Expenses Description 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','Account Name','Expense Group','Cost Behavior','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.accountName||'', r.expenseGroup||'', r.costBehavior||'', r.requester||'']);
    const stamp = localToday();   // YYYY-MM-DD
    window.exportXlsx('ExpensesDescription_Master_'+stamp+'.xlsx', head, rows, { sheetName:'Expenses Description' });
    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 General Information 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:'กรอก General Information ให้ครบก่อน Import', ttl:4500 });
      return;
    }
    // 1) Must have pressed "Next: Expenses Details" (section unlocked & editable).
    if (!expensesEditable){
      window.toast('กรุณากดปุ่ม "Next: Expenses Details" ในกรอบ General Information ก่อน Import', {tone:'warn', title:'ยังไม่ได้ปลดล็อก Expenses Details'});
      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');
    // Validation: every Expense Type must match a Master accountName in the Database.
    const masterSet = new Set(ensureDescDB().map(r => (r.accountName||'').trim()).filter(Boolean));
    const bad = [...new Set(importedRows.map(r => (r.expenseGroup||'').trim()).filter(v => v && !masterSet.has(v)))];
    if (bad.length){
      throw new Error('พบ Expense Type ที่ไม่ตรงกับ Master accountName ใน Database จำนวน ' + bad.length + ' รายการ ?  กรุณาแก้ไขให้ตรงกับรายการใน Database :  ' + bad.map(v=>'“'+v+'”').join(' , '));
    }
    // Condition 2: every Date in the file must match Request Date
    const reqDate = (form.reqDate||'').slice(0,10);
    if (!reqDate){
      throw new Error('กรุณาระบุ Request Date ในหน้า General Information ก่อนนำเข้าไฟล์');
    }
    const badDates = [...new Set(importedRows.map(r => r.date||'').filter(d => d && d !== reqDate))];
    const missingDate = importedRows.some(r => !r.date);
    if (missingDate){
      throw new Error('พบแถวที่ไม่มีวันที่ในไฟล์ Import — คอลัมน์ Date ต้องกรอกให้ครบและตรงกับ Request Date (' + fmtDateFull(reqDate) + ')');
    }
    if (badDates.length){
      throw new Error('วันที่ในไฟล์ Import ไม่ตรงกับวันที่ Request Date\nRequest Date : ' + fmtDateFull(reqDate) + ' - วันที่ในไฟล์ Import : ' + badDates.map(d=>fmtDateFull(d)).join(', '));
    }
    // Estimate/Actual must be valid, non-negative numbers
    const numOk = s => { const v=String(s==null?'':s).replace(/[,\s"]/g,''); return v===''||/^-?\d+(\.\d+)?$/.test(v); };
    const badNum = importedRows.some(r => !numOk(r.rawEstimate) || !numOk(r.rawActual));
    if (badNum){
      throw new Error('พบค่า Estimate หรือ Actual ที่ไม่ใช่ตัวเลขที่ถูกต้อง — กรุณาตรวจสอบ (ห้ามมีตัวอักษรหรือสัญลักษณ์ปนมา)');
    }
    const negNum = importedRows.some(r => (Number(r.estimate)||0) < 0 || (Number(r.amount)||0) < 0);
    if (negNum){
      throw new Error('พบค่า Estimate หรือ Actual ติดลบ — ค่าต้องเป็น 0 หรือมากกว่า');
    }
    // Fiscal Month/Year in the file (derived from Date) must match the form
    if (form.fm && form.fy){
      const badPeriod = [...new Set(importedRows.map(r => r.date).filter(Boolean).filter(d => {
        const dt = new Date(d);
        return MONTH_NAMES[dt.getMonth()] !== form.fm || String(dt.getFullYear()) !== String(form.fy);
      }).map(d => fmtDateFull(d)))];
      if (badPeriod.length){
        throw new Error('วันที่ในไฟล์ไม่ตรงกับงวด Fiscal ' + form.fm + ' ' + form.fy + ' ที่เลือกในฟอร์ม — พบ: ' + badPeriod.join(', '));
      }
    }
    // Accounts Payable is required
    const missingPayable = importedRows.filter(r => !(r.payable||'').trim()).length;
    if (missingPayable){
      throw new Error('พบ ' + missingPayable + ' แถวที่ไม่มีข้อมูล Accounts Payable — คอลัมน์นี้ต้องกรอกให้ครบทุกแถว');
    }
    const stamped = importedRows
      .filter(r => r.expenseGroup)   // keep every row that has an Expense Type
      .map((r,i) => ({ ...r, _rid: 'row_'+Date.now()+'_'+i, item: r.item ?? (i+1), entryDate: entryISO,
        costBehavior: (CAT_META[(r.expenseGroup||'').trim()]||{}).costBehavior || '' }));
    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 Expenses · General Information', '');
    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}}>นำเข้าข้อมูล Expenses Details</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.expenseGroup||'').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>
      <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.55,whiteSpace:'pre-line'}}>
        <span>{(err && err.message) || String(err)}</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 "Expenses_Import_Template"
    const baseName = file.name.replace(/\.[^.]+$/,'').trim();
    if (baseName !== 'Expenses_Import_Template'){
      showImportError(new Error('ชื่อไฟล์ไม่ถูกต้อง — ต้องเป็น “Expenses_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[3]||'').replace(/["\s]/g,'')); const act=parseFloat((cols[4]||'').replace(/["\s]/g,'')); return cols.length>=5 && (cols[1]||'').trim() && (!isNaN(est) || !isNaN(act)); });
        if (!dataLines.length) throw new Error('ไม่พบข้อมูลในไฟล์ กรุณาตรวจสอบ Template (Date, Expense Type, Description, Estimate, Actual, Accounts Payable)');
        const imported = [];
        dataLines.forEach(l => {
          const cols = l.split(',').map(c => c.replace(/"/g,'').trim());
          const est = parseFloat(cols[3]); const act = parseFloat(cols[4]);
          if (cols[1] && (!isNaN(est) || !isNaN(act))) imported.push({
            date: normImportDate(cols[0]||''),
            expenseGroup: cols[1], accountName: cols[2]||'',
            estimate: isNaN(est)?0:est, amount: isNaN(act)?0:act,
            rawEstimate: cols[3]||'', rawActual: cols[4]||'', payable: cols[5]||'' });
        });
        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 ? 'ยังไม่มีการแก้ไข — แก้ไขข้อมูลใน Expenses Details ก่อน' : ''))}
            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}}>
        {/* General Information pane — same as General Information 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></label>
              <div className="dateinput" style={submittedLock ? {opacity:.55,pointerEvents:'none',background:'#f1f3f9'} : {}}>
                <I.Cal size={14} stroke="#7b87a8"/>
                <input type="date" value={form.reqDate} disabled={submittedLock} onChange={e=>setGeneral('reqDate',e.target.value)}/>
              </div>
            </div>
            <div className="field">
              <label>Currency <span className="req">*</span> <span style={{color:'#7b87a8',fontWeight:500,fontSize:11,marginLeft:4}}>(locked)</span></label>
              <div style={{position:'relative'}}>
                <input className="input mono" value={form.currency} readOnly
                       placeholder="— กด Add Item —"
                       title="Currency ถูกล็อคไว้เป็น THB"
                       style={{
                         background: form.currency ? 'linear-gradient(135deg,#eef1f8,#fff)' : '#fafbff',
                         color: form.currency ? '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>Company name <span className="req">*</span></label>
              <select className="select" value={form.company} onChange={e=>setGeneral('company',e.target.value)}
                disabled={submittedLock}
                title={submittedLock ? 'รายการที่ Submit แล้ว ไม่สามารถเปลี่ยน Company ได้' : ''}>
                <option value="">— Select —</option>
                {['เตือนใจพาณิชย์กรุ๊ป','มือหนึ่งอินเตอร์กรุ๊ป','เจ.เอส.2020'].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" ในกรอบ Expenses Details ก่อนจึงจะแก้ไขได้' : ''}
                    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;
                      }
                      setExpensesUnlocked(true);
                      setReviewUnlocked(false);
                      setStep(2);
                      window.toast('Expenses Details ปลดล็อคแล้ว · เริ่มกรอกค่าใช้จ่ายได้', {tone:'success'});
                    }}>
              Next: {detailLabel} <I.Chev size={12}/>
            </button>
          </div>
        </Pane>

        {/* Expenses Details pane — long list */}
        <Pane title={detailLabel} open={open2} onToggle={()=>setOpen2(!open2)} accent>
          {/* Wrapper that grays out when not on step 2 */}
          <div style={{
            opacity: expensesEditable ? 1 : 0.45,
            filter:  expensesEditable ? 'none' : 'grayscale(.55)',
            pointerEvents: expensesEditable ? '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="ค้นหา Expense Type, Description, Accounts Payable…" value={search} onChange={e=>setSearch(e.target.value)}/>
            </div>
            <span className="chip" style={{marginRight:24}}>{expenseFilteredRows.length} of {itemCount} rows</span>
            <div className="row" style={{gap:8}}>
              <button className="btn" onClick={onNewExpenseItem} 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 || !expensesEditable || !!editingId}
                title={editingId ? 'อยู่ในโหมดแก้ไขรายการ — ไม่สามารถล้างทั้งหมดได้' : ((itemCount>0 && !expensesEditable) ? 'ฟอร์มถูกล็อก (ผ่านขั้น Review แล้ว) — ปลดล็อกก่อนจึงจะล้างได้' : '')}
                style={(itemCount===0 || !expensesEditable || !!editingId) ? {opacity:.5,cursor:'not-allowed'} : {}}><I.Trash size={14}/> Clear all</button>
            </div>
          </div>

          {/* Expenses table — uniform cell typography */}
          <div className="tbl-wrap" style={{border:'none',borderRadius:0}}>
            <table className="t se-exp-table" style={{tableLayout:'auto',width:'100%'}}>
              <thead>
                <tr>
                  <th style={{textAlign:'center',width:'9%'}}>Date</th>
                  <th className="se-sort" style={{width:'14%'}} onClick={()=>toggleSort('expenseGroup')}>Expense Type{sortCaret(sort,'expenseGroup')}</th>
                  <th className="se-sort" style={{width:'15%'}} onClick={()=>toggleSort('accountName')}>Description{sortCaret(sort,'accountName')}</th>
                  <th className="num se-sort" style={{width:'10%'}} onClick={()=>toggleSort('estimate')}>Estimate{sortCaret(sort,'estimate')}</th>
                  <th className="num se-sort" style={{width:'10%'}} onClick={()=>toggleSort('amount')}>Actual{sortCaret(sort,'amount')}</th>
                  <th className="num" style={{width:'10%'}}>Variance</th>
                  <th className="num" style={{width:'11%'}}>Cost Variance</th>
                  <th style={{width:'13%'}}>Accounts Payable</th>
                  <th style={{textAlign:'center',width:'8%'}}>Status</th>
                  {expensesEditable && <th style={{textAlign:'center',width:56}}></th>}
                </tr>
              </thead>
              <tbody>
                {expenseSortedRows.map((r,i)=>(
                  <tr key={r._rid||r.item||r.accountName||i} style={{animation:`fadein .25s ${Math.min(i,30)*25}ms both`}}>
                    <td className="mono" style={{textAlign:'center'}}>{fmtDateFull(form.reqDate)}</td>
                    <td className="thai" title={r.expenseGroup||''}>{r.expenseGroup||'—'}</td>
                    <td className="thai" title={r.accountName||''}>{r.accountName||'—'}</td>
                    <td className="num">
                      <ExpenseAmountCell value={r.estimate} editable={expensesEditable} tone="est"
                        onCommit={val => updateRowEstimate(r._rid, val)}/>
                    </td>
                    <td className="num">
                      <ExpenseAmountCell value={r.amount} editable={expensesEditable} tone="act"
                        onCommit={val => updateRowAmount(r._rid, val)}/>
                    </td>
                    {(()=>{ const est=Number(r.estimate)||0, act=Number(r.amount)||0, varc=act-est;
                      const pct = est!==0 ? (varc/est)*100 : 0;
                      const clr = varc===0?'#c3c9da':(varc>0?'#dc2626':'#059669');
                      return <React.Fragment>
                        <td className="num mono" style={{fontWeight:700,color:clr}}>{varc===0?fmt(0):(varc>0?'+':'−')+fmt(Math.abs(varc))}</td>
                        <td className="num mono" style={{fontWeight:700,color:est===0?'#c3c9da':clr}}>{est===0?'0.00%':(varc>0?'+':varc<0?'−':'')+Math.abs(pct).toFixed(2)+'%'}</td>
                      </React.Fragment>; })()}
                    <td className="thai" title={r.payable||''}>{r.payable || '—'}</td>
                    <td style={{textAlign:'center'}}><StatusIcon status={rowStatus}/></td>
                    {expensesEditable && <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>
                ))}
                {expenseSortedRows.length===0 && (
                  <tr><td colSpan="10" 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}}>Estimate</div>
                <div className="mono" style={{fontSize:24,fontWeight:800,marginTop:2}}>{fmt(totalEstimate)} <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>
            {(()=>{ const tv=totalActual-totalEstimate; const tp=totalEstimate!==0?(tv/totalEstimate)*100:0;
              const tclr='#fff';
              return <div style={{display:'flex',gap:30,alignItems:'center'}}>
              <div style={{textAlign:'right'}}>
                <div style={{fontSize:11,letterSpacing:'.1em',textTransform:'uppercase',opacity:.7}}>Variance</div>
                <div className="mono" style={{fontSize:24,fontWeight:800,color:tclr}}>{tv===0?fmt(0):(tv>0?'+':'−')+fmt(Math.abs(tv))} <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}}>Cost Variance</div>
                <div className="mono" style={{fontSize:24,fontWeight:800,color:tclr}}>{totalEstimate===0?'0.00%':(tv>0?'+':tv<0?'−':'')+Math.abs(tp).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('กลับมาแก้ Expenses Details ได้อีกครั้ง', {tone:'info', title:'กลับสู่ Step 2'});
              } else {
                // Step 2 → Step 1: lock Expenses, return to General
                setExpensesUnlocked(false);
                setReviewUnlocked(false);
                setStep(1);
                window.toast('กลับไปแก้ General แล้ว · Expenses ถูกล็อกชั่วคราว', {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 ข้อมูลใน Expenses Details ก่อน' : ''}
              onClick={()=>{
              if (itemCount===0){ window.toast('กรุณา Add Item หรือ Import ข้อมูลใน Expenses Details ก่อน', {tone:'warn', title:'ยังไม่มีข้อมูล'}); return; }
              if (!expensesUnlocked){ window.toast('กรุณากด "Next: Expenses Details" ก่อน', {tone:'warn'}); return; }
              if (!isGeneralComplete){ window.toast('ยังขาด: ' + missingFields.map(k=>fieldLabels[k]).join(', '), {tone:'warn', title:'กรอก General Information ให้ครบก่อนเข้า 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 ExpenseRow({ cat, value, onChange, onClear, index }){
  const has = value > 0;
  const [draft, setDraft] = useState('');
  const [focused, setFocused] = useState(false);

  // Display: while focused show raw draft, otherwise show formatted
  const display = focused
    ? draft
    : (value===0 ? '' : fmt(value));

  return (
    <div style={{
      display:'flex',alignItems:'center',gap:10,padding:'8px 12px',
      background: has ? 'linear-gradient(90deg,#eef0ff,#f7f8fc)' : '#fff',
      border:'1px solid '+(has?'#d8def5':'var(--line)'),
      borderRadius:10,transition:'.18s',
      animation:`fadein .25s ${Math.min(index,30)*15}ms both`,
    }}>
      <div style={{
        width:28,height:28,borderRadius:8,flex:'none',display:'grid',placeItems:'center',
        background: has ? 'linear-gradient(135deg,#2cb8b0,#1f2a8e)' : '#f1f3fa',
        color: has ? '#fff' : '#7b87a8',fontWeight:700,fontSize:11,
      }}>{index+1}</div>
      <div className="thai" style={{flex:1,fontSize:13,fontWeight:600,color:'var(--ink)',minWidth:0,
        whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{cat}</div>
      <input className="input mono expense-input"
        type="text" inputMode="decimal"
        style={{
          height:34,width:140,textAlign:'right',padding:'0 10px',
          fontWeight:700,color: has?'#1f2a8e':'#9aa3c2',
          background: has ? '#fff' : 'transparent',
        }}
        value={display}
        placeholder={focused ? '' : '0.00'}
        onFocus={(e)=>{
          setDraft(value === 0 ? '' : String(value));
          setFocused(true);
          setTimeout(()=> e.target.select(), 0);
        }}
        onChange={(e)=>{
          // Allow digits, single dot, and optional leading minus
          const raw = e.target.value.replace(/[^\d.\-]/g,'');
          // keep only first dot
          const parts = raw.split('.');
          const cleaned = parts.length > 1
            ? parts[0] + '.' + parts.slice(1).join('').slice(0,2)
            : raw;
          setDraft(cleaned);
          const n = parseFloat(cleaned);
          onChange(isNaN(n) ? 0 : n);
        }}
        onKeyDown={(e)=>{
          if (e.key === 'Enter'){
            e.preventDefault();
            // commit + format
            const n = parseFloat(draft);
            const final = isNaN(n) ? 0 : Math.round(n * 100) / 100;
            onChange(final);
            setFocused(false);   // shows formatted "1,234.56"
            e.target.blur();
            // jump to next expense input
            setTimeout(()=>{
              const inputs = Array.from(document.querySelectorAll('.expense-input'));
              const idx = inputs.indexOf(e.target);
              if (idx >= 0 && inputs[idx+1]) inputs[idx+1].focus();
            }, 0);
          }
        }}
        onBlur={()=>{
          setFocused(false);
          // commit: round to 2 decimals
          const n = parseFloat(draft);
          onChange(isNaN(n) ? 0 : Math.round(n * 100) / 100);
        }}/>
      <span style={{fontSize:11,color:'var(--muted)',fontWeight:600,width:24}}>THB</span>
      <button className="icon-btn" style={{width:28,height:28,opacity:has?1:0.3}}
              disabled={!has} onClick={onClear} title="Clear">
        <I.Reset size={12}/>
      </button>
    </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 ExpenseItemModal({ 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 [grp, setGrp] = React.useState('');
  const [beh, setBeh] = 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:'',grp:'',beh:''});   // 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 expense 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 an Account Name: trim + collapse internal whitespace.
  const normName = (s)=> String(s||'').replace(/\s+/g,' ').trim();
  const BEHAVIORS = ['Fixed Cost','Variable Cost','Other'];
  // Expense Group options — base (template + DB) ∪ custom, minus user-removed.
  const [groups, setGroups] = useState(()=>computeGroups());
  const GROUPS = groups;
  const [grpDialog, setGrpDialog] = useState(null);   // null | {mode:'add'} | {mode:'delete', name}
  const [grpInput, setGrpInput] = useState('');
  const [grpShake, setGrpShake] = useState(false);
  const nudgeGrpInput = ()=>{ setGrpShake(true); setTimeout(()=>setGrpShake(false), 500); };
  const rebuildGroups = ()=> setGroups(computeGroups());
  const commitAddGroup = (raw)=>{
    const name = String(raw||'').trim();
    if(!name){ window.toast('กรุณากรอกชื่อกลุ่ม', {tone:'warn', title:'ข้อมูลไม่ครบ'}); return; }
    if(/[0-9๐-๙]/.test(name)){ window.toast('ชื่อกลุ่มห้ามมีตัวเลข', {tone:'warn', title:'รูปแบบไม่ถูกต้อง'}); nudgeGrpInput(); return; }
    if(computeGroups().some(g=>g.toLowerCase()===name.toLowerCase())){ window.toast('มีกลุ่ม "'+name+'" อยู่แล้ว', {tone:'warn', title:'ซ้ำ'}); return; }
    const custom = loadCustomGroups(); custom.push(name); saveCustomGroups(custom);
    const removed = loadRemovedGroups().filter(g=>g!==name); saveRemovedGroups(removed);
    rebuildGroups(); setGrpDialog(null); setGrpInput('');
    window.toast('เพิ่มกลุ่ม "'+name+'" สำเร็จ', {tone:'success', title:'Expense Group'});
  };
  const openAddGroup = ()=>{ setGrpInput(''); setGrpDialog({mode:'add'}); };
  const askDeleteGroup = (name)=>{ const g=String(name||'').trim(); if(!g){ window.toast('เลือกกลุ่มที่ต้องการลบก่อน', {tone:'warn'}); return; } setGrpDialog({mode:'delete', name:g}); };
  const confirmDeleteGroup = (g)=>{
    const custom = loadCustomGroups();
    if(custom.includes(g)){ saveCustomGroups(custom.filter(x=>x!==g)); }
    else { const removed = loadRemovedGroups(); if(!removed.includes(g)){ removed.push(g); saveRemovedGroups(removed); } }
    rebuildGroups(); if(grp.trim()===g) setGrp(''); setGrpDialog(null);
    window.toast('ลบกลุ่ม "'+g+'" สำเร็จ', {tone:'info', title:'Expense Group'});
  };
  // 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 one of the three editable fields.
  const editChanged = acc.trim()!==orig.acc || grp.trim()!==orig.grp || beh.trim()!==orig.beh;
  const canSave   = canManage && fieldsEnabled && acc.trim() !== '' && grp.trim() !== '' && beh.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()||grp.trim()||beh.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(''); setGrp(''); setBeh(''); setCreateDate(''); setReqr(requester||''); setOrig({acc:'',grp:'',beh:''}); 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(''); setGrp(''); setBeh(''); setCreateDate(today); setReqr(requester||''); setOrig({acc:'',grp:'',beh:''});
    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.accountName||''); setGrp(row.expenseGroup||''); setBeh(row.costBehavior||'');
    setCreateDate(row.createDate||today);
    setReqr(row.requester||'');                 // show the original creator from DB
    setOrig({acc:(row.accountName||'').trim(), grp:(row.expenseGroup||'').trim(), beh:(row.costBehavior||'').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), accountName:normName(acc), expenseGroup:grp.trim(),
                     costBehavior:beh.trim(), 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), grpV = grp.trim(), behV = beh.trim();
    if (!accV){ window.toast('กรุณากรอก Account Name', {tone:'warn', title:'ข้อมูลไม่ครบ'}); return; }
    if (accV.length < 2){ window.toast('Account Name สั้นเกินไป · กรุณาระบุอย่างน้อย 2 ตัวอักษร', {tone:'warn', title:'ข้อมูลไม่ถูกต้อง'}); return; }
    if (!grpV){ window.toast('กรุณากรอก Expense Group', {tone:'warn', title:'ข้อมูลไม่ครบ'}); return; }
    if (!behV){ window.toast('กรุณาเลือก Cost Behavior', {tone:'warn', title:'ข้อมูลไม่ครบ'}); return; }
    if (mode==='edit' && !editChanged){ window.toast('ไม่มีการเปลี่ยนแปลง · แก้ไขค่าก่อนจึงจะบันทึกได้', {tone:'warn', title:'ไม่มีการแก้ไข'}); return; }
    // Duplicate guard: same Account Name on a DIFFERENT item is not allowed.
    const dup = ensureDescDB().find(r => (r.accountName||'').trim().toLowerCase() === accV.toLowerCase()
                             && Number(r.item) !== Number(itemNo));
    if (dup){ window.toast(`Account 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>
      )}
      {grpDialog && (
        <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) setGrpDialog(null); }}>
          <div style={{width:380,maxWidth:'90vw',background:'#fff',borderRadius:16,padding:'22px 24px',boxShadow:'0 24px 60px -12px rgba(16,24,40,.4)',animation:'fadein .18s both'}}>
            {grpDialog.mode==='add' ? (
              <div style={{display:'grid',gap:16}}>
                <div style={{display:'flex',alignItems:'center',gap:12}}>
                  <span style={{width:30,height:30,borderRadius:99,flexShrink:0,display:'grid',placeItems:'center',background:'linear-gradient(135deg,#3a56d4,#1f2a8e)',boxShadow:'0 0 0 4px rgba(58,86,212,.18), 0 4px 12px -2px rgba(31,42,142,.55)'}}>
                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{display:'block'}}><g transform="translate(0.5 0)"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M8 13h8M8 17h5"/></g></svg>
                  </span>
                  <span style={{fontSize:16,fontWeight:800,color:'var(--ink)'}}>Expense Group</span>
                </div>
                <div className="field">
                  <label>เพิ่มกลุ่มค่าใช้จ่าย</label>
                  <input className={"input thai"+(grpShake?" gi-shake":"")} autoFocus value={grpInput} placeholder="เช่น ค่าซ่อมบำรุง"
                    onChange={e=>setGrpInput(e.target.value.replace(/[0-9๐-๙]/g,''))}
                    onKeyDown={e=>{ if(e.key==='Enter'){ e.preventDefault(); nudgeGrpInput(); } if(e.key==='Escape'){ setGrpDialog(null); } }}/>
                </div>
                <div style={{display:'flex',justifyContent:'flex-end',gap:8}}>
                  <button type="button" className="btn" onClick={()=>setGrpDialog(null)}>Cancel</button>
                  <button type="button" className="btn primary" onClick={()=>commitAddGroup(grpInput)}>Add</button>
                </div>
              </div>
            ) : (
              <div style={{display:'grid',gap:18}}>
                <div style={{fontSize:15,fontWeight:800,color:'var(--ink)'}}>Delete <span style={{color:'#b91c1c'}}>กลุ่มค่าใช้จ่ายนี้ ?</span></div>
                <div style={{fontSize:13,display:'flex',alignItems:'center',gap:12,color:'var(--ink-2)'}}>
                  <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>ต้องการลบ Expense Group <b className="thai" style={{color:'#1f2a8e'}}>{grpDialog.name}</b> ออกจากระบบหรือไม่ ?</span>
                </div>
                <div style={{display:'flex',justifyContent:'flex-end',gap:8}}>
                  <button type="button" onClick={()=>setGrpDialog(null)} className="btn">Cancel</button>
                  <button type="button" onClick={()=>confirmDeleteGroup(grpDialog.name)} className="btn danger">Delete</button>
                </div>
              </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>

      {/* Row 1: Item · Create date · Requester */}
      <div style={{display:'grid',gridTemplateColumns:'1fr 1fr 1fr',gap:12}}>
        <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"/>)}
      </div>

      {/* Row 2: Account Name · Expense Group · Cost Behavior */}
      <div style={{display:'grid',gridTemplateColumns:'1fr 1fr 1fr',gap:12}}>
        <div className="field">
          <label style={{minHeight:22}}>Account Name</label>
          <input className="input thai" value={acc} disabled={!fieldsEnabled}
            onChange={e=>setAcc(e.target.value)} placeholder={'เช่น ค่าเช่า - สาขาใหม่'}
            style={!fieldsEnabled ? {background:'#f1f3f9',color:'#9aa3c2',cursor:'not-allowed'} : {}}/>
        </div>
        <div className="field">
          <div style={{display:'flex',alignItems:'center',justifyContent:'space-between',minHeight:22}}>
            <label style={{margin:0}}>Expense Group</label>
            <div style={{display:'flex',gap:4,marginRight:14}}>
              <button type="button" title="เพิ่มกลุ่มใหม่" disabled={!fieldsEnabled||!canManage} onClick={openAddGroup} className="gi-grp-btn"
                style={{color:'#2e3bbf',cursor:(!fieldsEnabled||!canManage)?'not-allowed':'pointer',opacity:(!fieldsEnabled||!canManage)?.35:1}}>
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round"><path d="M12 5v14M5 12h14"/></svg>
              </button>
              <button type="button" title="ลบกลุ่มที่เลือก" disabled={!fieldsEnabled||!canManage||!grp.trim()} onClick={()=>askDeleteGroup(grp)} className="gi-grp-btn"
                style={{color:'#d0475f',cursor:(!fieldsEnabled||!canManage||!grp.trim())?'not-allowed':'pointer',opacity:(!fieldsEnabled||!canManage||!grp.trim())?.35:1}}>
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14"/></svg>
              </button>
            </div>
          </div>
          <select className="select thai" value={GROUPS.includes(grp)?grp:''} disabled={!fieldsEnabled}
            onChange={e=>setGrp(e.target.value)} style={!fieldsEnabled ? {background:'#f1f3f9',color:'#9aa3c2',cursor:'not-allowed'} : {}}>
            <option value="">— Select —</option>
            {GROUPS.map(g => <option key={g} value={g}>{g}</option>)}
          </select>
        </div>
        <div className="field">
          <label style={{minHeight:22}}>Cost Behavior</label>
          <select className="select" value={beh} disabled={!fieldsEnabled}
            onChange={e=>setBeh(e.target.value)}
            style={!fieldsEnabled ? {background:'#f1f3f9',color:'#9aa3c2',cursor:'not-allowed'} : {}}>
            <option value="">— Select —</option>
            {BEHAVIORS.map(b => <option key={b} value={b}>{b}</option>)}
          </select>
        </div>
      </div>
    </div>
  );
}

// Small "Locked" chip shown on Approved records in the Edit existing request list.
function LockBadge(){
  return (
    <span style={{display:'inline-flex',alignItems:'center',gap:4,fontSize:9.5,fontWeight:700,padding:'2.5px 8px',borderRadius:99,background:'#fee2e2',color:'#b91c1c',border:'1px solid #fecaca',textTransform:'uppercase',letterSpacing:'.04em',flexShrink:0}}>
      <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.8" strokeLinecap="round" strokeLinejoin="round"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>
      Locked
    </span>
  );
}

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 [fCompany, setFCompany] = 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_expense_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);
        return { id, company:r.company||'—', date:r.reqDate||'', 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 companies = [...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 =>
    (!fCompany || r.company===fCompany) &&
    (!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, fCompany, 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'}
    : s==='Approved'
    ? {background:'#dbeafe',color:'#1d4ed8',border:'1px solid #bfdbfe'}
    : s==='Rejected'
    ? {background:'#ffedd5',color:'#c2410c',border:'1px solid #fed7aa'}
    : {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="ค้นหา Company, Date หรือ Period" value={q} onChange={e=>setQ(e.target.value)}/>
        </div>
        <select className="select" value={fCompany} onChange={e=>setFCompany(e.target.value)} style={{fontSize:12.5,paddingRight:52,flex:'0 0 auto',width:'auto',minWidth:170}}>
          <option value="">All Company</option>
          {companies.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 lockedRec = r.status==='Approved';
          const on = !lockedRec && 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} title={lockedRec ? 'รายการนี้อนุมัติแล้ว — ต้อง Reopen ที่เมนู Approve ก่อนจึงจะแก้ไขได้' : undefined}
              onClick={()=>{ if (lockedRec){ window.toast('รายการนี้อนุมัติแล้ว จึงถูกล็อกไม่ให้แก้ไข — กรุณา Reopen ที่เมนู Approve ก่อน', {tone:'warn', title:'รายการถูกล็อก'}); return; } 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: lockedRec ? 'not-allowed' : 'pointer',borderRadius:13,overflow:'hidden',
                border: on ? c.border : '1px solid #e5e9f2',
                background: lockedRec ? 'linear-gradient(135deg,#eff6ff 0%,#f6f9ff 100%)' : (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: lockedRec ? 'linear-gradient(180deg,#60a5fa,#1d4ed8)' : 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',opacity: lockedRec ? .65 : 1,
                boxShadow: on ? c.glow : 'none', transition:'all .16s ease'}} className="thai">
                {({'เตือนใจพาณิชย์กรุ๊ป':'TJ','มือหนึ่งอินเตอร์กรุ๊ป':'MNI','เจ.เอส.2020':'JS'}[r.company] || (r.company||'—').replace(/\s/g,'').slice(0,2))}
              </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>
                  {lockedRec && <LockBadge/>}
                </div>
                <div style={{fontSize:11.5,color:'var(--muted)',marginTop:3,display:'flex',gap:6,alignItems:'center'}}>
                  <span className="mono">{fmtDateFull(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>
              {lockedRec ? (
                <div style={{width:22,height:22,borderRadius:99,flexShrink:0,display:'flex',alignItems:'center',justifyContent:'center',lineHeight:0,background:'linear-gradient(135deg,#f87171,#b91c1c)',boxShadow:'0 3px 8px -4px rgba(185,28,28,.5)'}}>
                  <svg width="13.5" height="13.5" viewBox="0 0 24 24" style={{display:'block'}} fill="none" stroke="#fff" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><rect x="4.5" y="10.5" width="15" height="9.5" rx="2"/><path d="M8 10.5V7a4 4 0 0 1 8 0v3.5"/></svg>
                </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 SellingPreview({ form, total, totalEstimate, totalActual, itemCount }){
  const meta = [
    ['Date', fmtDateFull(form.reqDate)],
    ['Currency', form.currency],
    ['Company name', form.company],
    ['Period', form.fm+' '+form.fy],
  ];
  const rows = (form.rows||[]).slice();
  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}}>Estimate</div>
            <div className="mono" style={{fontSize:28,fontWeight:800,marginTop:4}}>{fmt(totalEstimate)} <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}}>Expenses 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 expenses 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={{minWidth:0,flex:1}}>Description</span>
            <span style={{width:110,textAlign:'right',flex:'none'}}>Estimate</span>
            <span style={{width:110,textAlign:'right',flex:'none'}}>Actual</span>
            <span style={{width:120,textAlign:'right',flex:'none'}}>Variance</span>
          </div>
        )}
        {rows.map((r,i)=>{
          const est=Number(r.estimate)||0, act=Number(r.amount)||0, varc=est-act;
          return (
          <div key={i} style={{display:'flex',justifyContent:'space-between',padding:'8px 12px',background:i%2?'#f7f8fc':'#fff',fontSize:13,gap:10}}>
            <span className="thai" style={{minWidth:0,flex:1,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{r.accountName}</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:120,textAlign:'right',flex:'none',fontWeight:700,color:varc===0?'#c3c9da':(varc>0?'#059669':'#dc2626')}}>{varc===0?fmt(0):(varc>0?'+':'−')+fmt(Math.abs(varc))}</span>
          </div>
          );
        })}
      </div>
    </div>
  );
}

window.GeneralInformationView = GeneralInformationView;
