// Shared UI Components + CRUDPage
const { useState, useEffect, useRef, useMemo, useCallback, useContext } = React;

// ── Context ──────────────────────────────────────────────
const AppCtx = React.createContext({});
window.AppCtx = AppCtx;

// ── Global Floating Overlay System ───────────────────────────────────────────
// Single source of truth for EVERY dropdown/menu/popover in the ERP (filter
// selects, searchable selects, company switcher, user menu, period pickers,
// challan search, etc). Renders its content through a React portal straight
// onto <body> with `position:fixed` coordinates computed live from the
// trigger element's bounding rect — this is what guarantees the panel always
// paints on the top-most layer and is never clipped by an ancestor's
// `overflow:hidden`, `transform` (which silently creates a new stacking
// context), or a lower z-index card/table/sticky-header. No per-module
// z-index bumping is needed — every floating panel in the app shares this
// one component and therefore one consistent stacking rule.
//
// Usage: replace a panel that was `position:'absolute'` inside a
// `position:'relative'` trigger wrapper with:
//   <FloatingLayer anchorRef={wrapRef} open={open} align="left|right" matchWidth>
//     ...exact same panel markup...
//   </FloatingLayer>
// Keep the wrapper's ref (wrapRef) — FloatingLayer reads its rect every open,
// scroll and resize. Outside-click handlers must also check the portaled
// panel's own ref (see PANEL_LAYER_ZINDEX below) in addition to the trigger.
window.PANEL_LAYER_ZINDEX = 9500; // above modals (1000-1050), below toasts (9999)
function FloatingLayer({ anchorRef, open, placement, align, matchWidth, minWidth, offset, zIndex, panelRef, style, children }) {
  const [geo, setGeo] = React.useState(null);
  React.useLayoutEffect(() => {
    if (!open || !anchorRef || !anchorRef.current) { setGeo(null); return; }
    function update() {
      if (!anchorRef.current) return;
      const r = anchorRef.current.getBoundingClientRect();
      let dir = placement || 'down';
      if (!placement || placement === 'auto') {
        dir = (window.innerHeight - r.bottom) < 260 && r.top > 260 ? 'up' : 'down';
      }
      setGeo({ r, dir });
    }
    update();
    window.addEventListener('scroll', update, true);
    window.addEventListener('resize', update);
    return () => { window.removeEventListener('scroll', update, true); window.removeEventListener('resize', update); };
  }, [open, anchorRef, placement]);
  if (!open || !geo) return null;
  const { r, dir } = geo;
  const gap = offset == null ? 5 : offset;
  const base = {
    position: 'fixed',
    zIndex: zIndex || window.PANEL_LAYER_ZINDEX,
    ...(dir === 'up' ? { bottom: window.innerHeight - r.top + gap } : { top: r.bottom + gap }),
    ...(align === 'right' ? { right: window.innerWidth - r.right } : { left: r.left }),
    ...(matchWidth ? { width: r.width } : {}),
    ...(minWidth ? { minWidth } : {}),
  };
  return ReactDOM.createPortal(
    <div ref={panelRef} style={{ ...base, ...style }}>{children}</div>,
    document.body
  );
}
window.FloatingLayer = FloatingLayer;

// ── Export Menu ──────────────────────────────────────────────────────────────
// Same visible "Export" button as always. When the signed-in user holds the
// "Can Export Confidential Pricing" permission, clicking it reveals a small
// premium dropdown with Standard / Confidential Export — everyone else just
// gets the plain button they've always had, with no hint anything else exists.
const SVGExportIco = () => <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>;
const SVGShieldIco = () => <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 3l7 3v5c0 4.5-3 8.5-7 10-4-1.5-7-5.5-7-10V6l7-3z"/></svg>;
const SVGChevronIco = () => <svg width="9" height="6" viewBox="0 0 10 6" fill="none" style={{marginLeft:1,opacity:.6,flexShrink:0}}><path d="M1 1l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>;

function ExportMenu({ onStandard, onConfidential, canConfidential, label, className, size }) {
  const [open, setOpen] = React.useState(false);
  const wrapRef = React.useRef();
  const panelRef = React.useRef();

  React.useEffect(() => {
    if (!open) return;
    function handler(e) {
      if (wrapRef.current && !wrapRef.current.contains(e.target) &&
          panelRef.current && !panelRef.current.contains(e.target)) setOpen(false);
    }
    document.addEventListener('mousedown', handler);
    return () => document.removeEventListener('mousedown', handler);
  }, [open]);

  const btnClass = className || `btn btn-wh ${size || 'btn-sm'}`;

  // Unauthorized (or unauthenticated) users only ever see the plain button —
  // no dropdown chevron, no trace that a second export type exists.
  if (!canConfidential) {
    return (
      <button className={btnClass} onClick={onStandard}>
        <SVGExportIco /> {label || 'Export'}
      </button>
    );
  }

  return (
    <div ref={wrapRef} style={{ position: 'relative', display: 'inline-block' }}>
      <button className={btnClass} onClick={() => setOpen(o => !o)}>
        <SVGExportIco /> {label || 'Export'} <SVGChevronIco />
      </button>
      <FloatingLayer anchorRef={wrapRef} open={open} align="right" minWidth={210} panelRef={panelRef}
        style={{ background: '#fff', border: '1px solid var(--bdr)', borderRadius: 14, boxShadow: 'var(--sh-drop)', overflow: 'hidden', padding: 4, zIndex: window.PANEL_LAYER_ZINDEX }}>
        <div className="co-drop-it" style={{ borderRadius: 9 }} onMouseDown={() => { setOpen(false); onStandard(); }}>
          <SVGExportIco /> Standard Export
        </div>
        <div className="co-drop-it" style={{ borderRadius: 9 }} onMouseDown={() => { setOpen(false); onConfidential(); }}>
          <SVGShieldIco /> Confidential Export
        </div>
      </FloatingLayer>
    </div>
  );
}
window.ExportMenu = ExportMenu;

// ── Toast System ─────────────────────────────────────────
// Centralized, non-blocking notifications tuned for high-speed data entry.
// Success/info toasts auto-dismiss fast and never stack (latest replaces prior).
// Error toasts persist until the user dismisses them and stay fully readable.
const NOTIF_SPEEDS = { standard: 2000, fast: 800, highspeed: 550 };
function getNotifSpeed() {
  const v = localStorage.getItem('om_notif_speed');
  return NOTIF_SPEEDS[v] ? v : 'fast';
}
function setNotifSpeed(v) {
  if (!NOTIF_SPEEDS[v]) return;
  localStorage.setItem('om_notif_speed', v);
  window.dispatchEvent(new Event('om-notif-speed-change'));
}
window.getNotifSpeed = getNotifSpeed;
window.setNotifSpeed = setNotifSpeed;

let _toast = null;
let _toastSeq = 0;
function ToastHost() {
  const [items, setItems] = useState([]);
  const timers = useRef({});

  const remove = useCallback((id) => {
    setItems(p => p.map(t => t.id === id ? { ...t, exiting: true } : t));
    setTimeout(() => setItems(p => p.filter(t => t.id !== id)), 160);
  }, []);

  useEffect(() => {
    _toast = (msg, type='ok') => {
      const id = ++_toastSeq;
      const isErr = type === 'er';
      if (!isErr) {
        // Success/info never stack — replace whichever ok/in toast is showing.
        Object.keys(timers.current).forEach(k => {
          const t = timers.current[k];
          if (!t.isErr) { clearTimeout(t.handle); delete timers.current[k]; remove(Number(k)); }
        });
        setItems(p => [...p.filter(t => t.type === 'er'), { id, msg, type, exiting:false }]);
        const dur = NOTIF_SPEEDS[getNotifSpeed()];
        timers.current[id] = { isErr:false, handle: setTimeout(() => { delete timers.current[id]; remove(id); }, dur) };
      } else {
        // Errors persist until dismissed — keep visible, no auto-timeout.
        setItems(p => [...p, { id, msg, type, exiting:false }]);
        timers.current[id] = { isErr:true };
      }
    };
    window.toast = _toast;
  }, [remove]);

  const icons = { ok:<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--ok)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M5 13l4 4L19 7"/></svg>, er:<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>, in:<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg> };
  return (
    <div className="toast-r">
      {items.map(t => (
        <div key={t.id} className={`toast ${t.type}${t.exiting ? ' toast-exit' : ''}`}>
          <span style={{fontWeight:700}}>{icons[t.type]}</span>
          <span style={{flex:1}}>{t.msg}</span>
          {t.type === 'er' && (
            <button className="toast-close" aria-label="Dismiss" onClick={() => { clearTimeout(timers.current[t.id]?.handle); delete timers.current[t.id]; remove(t.id); }}>×</button>
          )}
        </div>
      ))}
    </div>
  );
}
window.ToastHost = ToastHost;

// ── Badge ────────────────────────────────────────────────
function Badge({ v }) {
  const map = {
    Active:'gn', Delivered:'gn', Completed:'gn', Approved:'gn',
    Inactive:'rd', Cancelled:'rd', Failed:'rd', Rejected:'rd',
    Pending:'yw', Draft:'yw',
    'In Transit':'bl', Processing:'bl',
    SUPER_ADMIN:'or', ADMIN:'pu', MANAGER:'bl',
    ACCOUNTANT:'gn', OPERATOR:'gy', VIEWER:'gy',
    Active_:'gn',
  };
  const cls = map[v] || 'gy';
  return <span className={`bdg bg-${cls}`}>{v}</span>;
}
window.Badge = Badge;

// ── Confirm Dialog ────────────────────────────────────────
function Confirm({ msg, onOk, onCancel }) {
  return (
    <div className="mbg" onClick={e => e.target===e.currentTarget && onCancel()}>
      <div className="mod mod-sm">
        <div className="mod-hd">
          <h2>Confirm Delete</h2>
          <button className="mod-x" onClick={onCancel}>×</button>
        </div>
        <div className="mod-bd">
          <p style={{color:'var(--txt2)',lineHeight:1.7}}>{msg || 'Are you sure? This action cannot be undone.'}</p>
        </div>
        <div className="mod-ft">
          <button className="btn btn-wh" onClick={onCancel}>Cancel</button>
          <button className="btn btn-rd" onClick={onOk}>Delete</button>
        </div>
      </div>
    </div>
  );
}
window.Confirm = Confirm;

// ── DiscardChangesModal — unsaved changes confirmation ────────────────────────
function DiscardChangesModal({ onContinue, onDiscard }) {
  return (
    <div className="mbg" style={{zIndex:1050}}>
      <div className="mod mod-sm">
        <div className="mod-hd"><h2>Unsaved Changes</h2></div>
        <div className="mod-bd">
          <p style={{color:'var(--txt2)',lineHeight:1.7,fontSize:13}}>
            You have unsaved changes.<br/>Are you sure you want to discard them?
          </p>
        </div>
        <div className="mod-ft">
          <button className="btn btn-wh" onClick={onContinue}>Continue Editing</button>
          <button className="btn btn-rd" onClick={onDiscard}>Discard Changes</button>
        </div>
      </div>
    </div>
  );
}
window.DiscardChangesModal = DiscardChangesModal;

// ── Level-0 Group Company Selector (shown only on OM GROUP dashboard) ──
// Renders a mandatory Company dropdown as the first field of a transaction
// form when the user is at Level 0 (companyId === 'group'). On Level-1
// company dashboards the company is already known, so this renders nothing.
function GroupCompanyField({ value, onChange }) {
  const companies = Store.all('companies');
  const v = (value && value !== 'group') ? value : '';
  return (
    <div style={{marginBottom:16,padding:'11px 14px',background:'#FFF9F5',border:'1px solid var(--or-bdr)',borderRadius:'var(--r)'}}>
      <div className="fld" style={{margin:0}}>
        <label style={{color:'var(--or)',fontWeight:700}}>Company <span className="req">*</span></label>
        <window.FormSelect placeholder="— Select Company —" value={v} onChange={onChange} options={companies.map(c=>({value:c.id,label:c.name}))}/>
        <span style={{fontSize:11,color:'var(--txt2)',marginTop:4,display:'block'}}>This transaction will be assigned to the selected company.</span>
      </div>
    </div>
  );
}
window.GroupCompanyField = GroupCompanyField;

// ── SearchableSelect — Premium ERPSelect — single global dropdown standard ────
// Shared by: Purchases · Sales · Transfers · Debris · Transporter · Vehicle
//            Stockyard · Diesel · all future modules
// API: options=[{value,label}], value, onChange(value,label),
//      placeholder, noOptionsMsg, inputStyle
// Visual: light panel · strong border+shadow · smooth hover · selected accent
function SearchableSelect({ options, value, onChange, placeholder, noOptionsMsg, inputStyle, disabled }) {
  const [open, setOpen] = React.useState(false);
  const [query, setQuery] = React.useState('');
  const [dropDir, setDropDir] = React.useState('down');
  const [hlIdx, setHlIdx] = React.useState(-1);
  const inputRef = React.useRef(null);
  const wrapRef  = React.useRef(null);
  const listRef  = React.useRef(null);
  const currentOpt = React.useMemo(() => options.find(o => o.value === value), [options, value]);
  const filtered = React.useMemo(() => {
    const q = query.trim().toLowerCase();
    if (!q) return options;
    return options.filter(o => o.label.toLowerCase().includes(q));
  }, [options, query]);
  React.useEffect(() => {
    if (!open) return;
    function handler(e) {
      if (wrapRef.current && !wrapRef.current.contains(e.target) &&
          listRef.current && !listRef.current.contains(e.target)) { setOpen(false); setQuery(''); }
    }
    document.addEventListener('mousedown', handler);
    return () => document.removeEventListener('mousedown', handler);
  }, [open]);
  React.useEffect(() => { if (!open) setHlIdx(-1); }, [open]);
  React.useEffect(() => {
    if (hlIdx < 0 || !listRef.current) return;
    const el = listRef.current.children[hlIdx];
    if (el) el.scrollIntoView({ block: 'nearest' });
  }, [hlIdx]);
  function calcDir() {
    if (wrapRef.current) {
      var r = wrapRef.current.getBoundingClientRect();
      setDropDir(window.innerHeight - r.bottom < 252 && r.top > 252 ? 'up' : 'down');
    }
  }
  function handleInputChange(e) { setQuery(e.target.value); if (!open) { calcDir(); } setOpen(true); }
  function handleFocus() { calcDir(); setOpen(true); setQuery(''); }
  function handleSelect(opt) { onChange(opt.value, opt.label); setOpen(false); setQuery(''); }
  function handleClear(e) {
    e.stopPropagation();
    onChange('', '');
    setQuery('');
    inputRef.current && inputRef.current.focus();
  }
  function handleKeyDown(e) {
    const count = filtered.length;
    if (!open) {
      if (e.key==='ArrowDown'||e.key==='Enter'||e.key===' ') {
        e.preventDefault(); calcDir(); setOpen(true); setHlIdx(0); setQuery('');
      }
      return;
    }
    if (e.key==='Escape') { e.preventDefault(); setOpen(false); setQuery(''); setHlIdx(-1); return; }
    if (e.key==='ArrowDown') { e.preventDefault(); setHlIdx(i=>i<count-1?i+1:i); return; }
    if (e.key==='ArrowUp')   { e.preventDefault(); setHlIdx(i=>i>0?i-1:0); return; }
    if (e.key==='Enter'&&hlIdx>=0&&filtered[hlIdx]) { e.preventDefault(); handleSelect(filtered[hlIdx]); return; }
    if (e.key==='Tab') { if(hlIdx>=0&&filtered[hlIdx])handleSelect(filtered[hlIdx]); setOpen(false); setQuery(''); setHlIdx(-1); }
  }
  const displayVal = open ? query : (currentOpt ? currentOpt.label : '');
  return (
    <div ref={wrapRef} style={{ position: 'relative', opacity: disabled ? .55 : 1 }}>
      {/* ── Trigger ── */}
      <div style={{
        display: 'flex', alignItems: 'center',
        border: `1.5px solid ${open ? 'var(--or)' : 'var(--bdr2)'}`,
        borderRadius: 6,
        background: disabled ? '#F3F4F6' : '#fff',
        transition: 'border-color .15s, box-shadow .15s',
        boxShadow: open ? '0 0 0 3px rgba(249,115,22,.11)' : '0 1px 3px rgba(0,0,0,.06)',
      }}>
        <input ref={inputRef} value={displayVal} onChange={handleInputChange} onFocus={handleFocus} onKeyDown={handleKeyDown}
          placeholder={placeholder || '— Search & Select —'} autoComplete="off" disabled={disabled}
          style={{
            flex: 1, border: 'none', outline: 'none',
            padding: '6px 10px', fontSize: 12.5,
            fontFamily: 'var(--font)', height: 36,
            color: 'var(--txt)', background: 'transparent',
            fontWeight: (currentOpt && !open) ? 600 : 400,
            cursor: disabled ? 'not-allowed' : 'pointer',
            ...(inputStyle || {}),
          }} />
        {value && !disabled && (
          <button type="button" onClick={handleClear} title="Clear"
            style={{ background: 'none', border: 'none', padding: '0 4px 0 0', cursor: 'pointer', color: 'var(--txt3)', fontSize: 16, lineHeight: 1, display: 'flex', alignItems: 'center', height: 36, transition: 'color .12s' }}>×</button>
        )}
        {/* Animated chevron */}
        <div style={{
          padding: '0 10px', display: 'flex', alignItems: 'center', height: 36,
          pointerEvents: 'none',
          transition: 'transform .2s',
          transform: open ? 'rotate(180deg)' : 'none',
        }}>
          <svg width="10" height="6" viewBox="0 0 10 6" fill="none">
            <path d="M1 1l4 4 4-4" stroke={open ? 'var(--or)' : 'var(--txt3)'} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
        </div>
      </div>
      {/* ── Dropdown panel — portaled to <body>, fixed-positioned, always top layer ── */}
      <FloatingLayer anchorRef={wrapRef} open={open && !disabled} placement={dropDir} matchWidth panelRef={listRef}
        style={{
          background: '#fff',
          border: '1px solid #E5E7EB',
          borderRadius: 8,
          boxShadow: '0 4px 8px rgba(0,0,0,.06), 0 16px 36px rgba(0,0,0,.12)',
          maxHeight: 252, overflowY: 'auto', overflowX: 'hidden',
          padding: '4px 0',
        }}>
        {filtered.length === 0
          ? <div style={{ padding: '14px 16px', fontSize: 12, color: 'var(--txt3)', textAlign: 'center', fontStyle: 'italic' }}>
              {noOptionsMsg || 'No results found'}
            </div>
          : filtered.map((opt, i) => (
            <div key={opt.value}
              className={`erp-drop-opt${opt.value === value ? ' erp-drop-sel' : ''}`}
              onMouseDown={e => { e.preventDefault(); handleSelect(opt); }}
              onMouseEnter={() => setHlIdx(i)}
              style={{
                background: i === hlIdx && opt.value !== value ? 'var(--or-lt)' : undefined,
                color: opt.value === value ? 'var(--or)' : i === hlIdx ? 'var(--or)' : 'var(--txt)',
                ...(inputStyle ? { fontFamily: inputStyle.fontFamily || 'var(--font)' } : {}),
              }}
            >{opt.label}</div>
          ))
        }
      </FloatingLayer>
    </div>
  );
}
window.SearchableSelect = SearchableSelect;

// ── Global Dropdown Standard — ERPSelect is the canonical alias ──────────────────
// Use <window.ERPSelect> for any new searchable dropdown anywhere in the ERP.
// Identical API to SearchableSelect: {options, value, onChange, placeholder}
window.ERPSelect = SearchableSelect;

// ── FiltSelect — compact SearchableSelect for .frow filter bars ──────────────────
// Drop-in replacement for <select className="fsel"> in any filter row.
// API: {options=[{value,label}], value, onChange(value), placeholder, style}
// Height: 32px matching .fsel standard. Fully searchable. Inherits ERPSelect visuals.
function FiltSelect({ options, value, onChange, placeholder, style }) {
  return (
    <div style={{ minWidth: 150, flexShrink: 0, ...style }}>
      <SearchableSelect
        options={options}
        value={value}
        onChange={onChange}
        placeholder={placeholder || 'All…'}
        inputStyle={{ height: 32, fontSize: 12, padding: '4px 9px', fontWeight: value ? 600 : 400 }}
      />
    </div>
  );
}
window.FiltSelect = FiltSelect;

// ── FormSelect — full-width SearchableSelect for form fields ─────────────────
// Drop-in replacement for <select className="sel"> inside a <div className="fld">.
// API: {options=[{value,label}], value, onChange(value), placeholder, disabled, style}
// Matches .sel sizing (38px, 13px, 9px radius padding) · inherits ERPSelect visuals.
function FormSelect({ options, value, onChange, placeholder, disabled, style }) {
  return (
    <div style={{ width: '100%', ...style }}>
      <SearchableSelect
        options={options}
        value={value}
        onChange={onChange}
        placeholder={placeholder || '— Select —'}
        disabled={disabled}
        inputStyle={{ height: 38, fontSize: 13, padding: '7px 12px', fontWeight: value ? 500 : 400 }}
      />
    </div>
  );
}
window.FormSelect = FormSelect;

// ── SmartTooltip — OM Group unified hover card ───────────────────────────────
// White card, 14px radius, soft shadow, thin border — matches Calendar style.
// Intelligently positions right→left→above→below based on available space.
// Touch: tap to open, tap outside to close.
function SmartTooltip({ children, content, width }) {
  const [pos, setPos] = React.useState(null);
  const refEl = React.useRef(null);
  const TIP_W = width || 240;
  const GAP = 10, MARGIN = 10, TIP_H_EST = 180;

  function openTip() {
    if (!refEl.current) return;
    var r = refEl.current.getBoundingClientRect();
    var vw = window.innerWidth, vh = window.innerHeight;
    // Prefer right of trigger; flip left if not enough room
    var left = r.right + GAP;
    if (left + TIP_W > vw - MARGIN) left = r.left - TIP_W - GAP;
    // Vertically centre on trigger; clamp to viewport
    var top = r.top + r.height / 2 - TIP_H_EST / 2;
    top  = Math.max(MARGIN, Math.min(top,  vh - TIP_H_EST - MARGIN));
    left = Math.max(MARGIN, left);
    setPos({ top, left });
  }

  // Touch: tap-to-toggle
  function handleTap(e) {
    e.stopPropagation();
    if (pos) { setPos(null); } else { openTip(); }
  }
  React.useEffect(function() {
    if (!pos) return;
    function dismiss() { setPos(null); }
    document.addEventListener('click', dismiss);
    return function() { document.removeEventListener('click', dismiss); };
  }, [pos]);

  return (
    <span ref={refEl}
      onMouseEnter={openTip}
      onMouseLeave={function(){setPos(null);}}
      onClick={handleTap}
      style={{position:'relative',display:'inline-flex',alignItems:'center',cursor:'help'}}>
      {children}
      {pos&&(
        <div className="om-tip" style={{
          top:pos.top, left:pos.left,
          width:TIP_W,
          maxHeight:'calc(100vh - 20px)',
          overflowY:'auto',
          padding:'12px 14px',
          fontSize:12,
        }}>
          {content}
        </div>
      )}
    </span>
  );
}
window.SmartTooltip = SmartTooltip;

// ── smartDropDir — utility: returns 'up' or 'down' for any dropdown ──────────
// Pass the wrapper DOM element; returns preferred open direction.
window.smartDropDir = function(wrapEl, dropH) {
  if (!wrapEl || !wrapEl.getBoundingClientRect) return 'down';
  var r = wrapEl.getBoundingClientRect();
  var h = dropH || 220;
  return (window.innerHeight - r.bottom) < h && r.top > (window.innerHeight - r.bottom) ? 'up' : 'down';
};

// Validate a company was picked at Level 0. Returns an error string or null.
window.requireGroupCompany = function(isGroup, companyId) {
  if (isGroup && (!companyId || companyId === 'group')) return 'Please select a Company for this transaction.';
  return null;
};

// ── CRUDPage ──────────────────────────────────────────────
function CRUDPage({
  title, subtitle, entityKey,
  columns, fields,
  searchFields = [], filters = [],
  modalSize = 'md',
  canAdd = true, canEdit = true, canDelete = true,
  addLabel, saveLabel, customActions, extraHeader,
  onFormChange,
}) {
  const { companyId } = useContext(AppCtx);
  const [items, setItems]       = useState([]);
  const [search, setSearch]     = useState('');
  const [fvals, setFvals]       = useState({});
  const [page, setPage]         = useState(1);
  const [modal, setModal]       = useState(null);
  const [editItem, setEditItem] = useState(null);
  const [form, setForm]         = useState({});
  const [delId, setDelId]       = useState(null);
  const PER = 10;

  useEffect(() => { load(); }, [companyId]);

  function load() { setItems(Store.all(entityKey, companyId)); }

  function resolveOpts(f) {
    if (f.options) return f.options;
    if (f.entityOptions) return Store.all(f.entityOptions, companyId).map(e => ({ value: e.id, label: e.name }));
    return [];
  }

  const filtered = useMemo(() => {
    return items.filter(item => {
      if (search) {
        const q = search.toLowerCase();
        if (!searchFields.some(k => String(item[k]||'').toLowerCase().includes(q))) return false;
      }
      for (const [k, v] of Object.entries(fvals)) {
        if (v && String(item[k]) !== String(v)) return false;
      }
      return true;
    });
  }, [items, search, fvals]);

  const totalPgs = Math.ceil(filtered.length / PER);
  const paged    = filtered.slice((page-1)*PER, page*PER);

  function openAdd() {
    const defs = {};
    fields.forEach(f => {
      if (f.type==='divider'||f.type==='section') return;
      defs[f.key] = f.default !== undefined ? f.default : '';
    });
    defs.companyId = companyId;
    setForm(defs); setEditItem(null); setModal('form');
  }

  function openEdit(item) {
    setForm({ ...item }); setEditItem(item); setModal('form');
  }

  function setField(k, v) {
    setForm(p => {
      const next = { ...p, [k]: v };
      if (onFormChange) onFormChange(next, k, setForm);
      return next;
    });
  }

  function handleSave(e) {
    e.preventDefault();
    if (editItem) {
      Store.update(entityKey, editItem.id, form);
      Store.addLog('UPDATE', title, `Updated: ${form.name||editItem.id}`);
    } else {
      Store.add(entityKey, form);
      Store.addLog('CREATE', title, `Created: ${form.name||'record'}`);
    }
    setModal(null); load();
    window.toast && window.toast(editItem ? 'Updated successfully' : 'Created successfully', 'ok');
  }

  function handleDelete() {
    Store.del(entityKey, delId);
    Store.addLog('DELETE', title, `Deleted record: ${delId}`);
    setDelId(null); load();
    window.toast && window.toast('Deleted successfully', 'ok');
  }

  function exportCSV() {
    const hdr = columns.map(c => c.label).join(',');
    const rows = filtered.map(item =>
      columns.map(c => {
        let v = item[c.key];
        if (c.entityKey) { const e=Store.byId(c.entityKey,v); v=e?e.name:v; }
        return `"${String(v||'').replace(/"/g,'""')}"`;
      }).join(',')
    ).join('\n');
    const blob = new Blob([hdr+'\n'+rows], {type:'text/csv'});
    const url  = URL.createObjectURL(blob);
    const a    = document.createElement('a');
    a.href=url; a.download=`${entityKey}_export.csv`; a.click();
    Store.addLog('EXPORT', title, `Exported ${filtered.length} records`);
    window.toast && window.toast('CSV exported', 'ok');
  }

  function renderCell(col, item) {
    const v = item[col.key];
    if (col.render) return col.render(v, item);
    if (col.entityKey) { const e=Store.byId(col.entityKey,v); return e?e.name:(v||'-'); }
    if (col.badge)     return <Badge v={String(v||'')} />;
    if (col.currency)  return v ? window.fmtCur(v) : '-';
    if (col.number)    return v !== undefined && v !== '' ? window.fmtNum(v) : '-';
    if (col.date)      return window.fmtDate(v);
    return v || '-';
  }

  const SVGSearch = () => <svg className="fs-ic" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>;
  const SVGPlus   = () => <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M12 5v14M5 12h14"/></svg>;
  const SVGExport = () => <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>;

  return (
    <div>
      {/* Page header */}
      <div className="ph">
        <div>
          <h1>{title}</h1>
          {subtitle && <p>{subtitle}</p>}
        </div>
        <div className="ph-act">
          {extraHeader}
          <button className="btn btn-wh" onClick={exportCSV}><SVGExport /> Export CSV</button>
          {canAdd && (
            <button className="btn btn-or" onClick={openAdd}>
              <SVGPlus /> {addLabel || `Add ${title.replace(/s$/,'')}`}
            </button>
          )}
        </div>
      </div>

      {/* Filters */}
      <div className="frow">
        <div className="fs">
          <SVGSearch />
          <input value={search} onChange={e=>{setSearch(e.target.value);setPage(1);}} placeholder="Search…" />
        </div>
        {filters.map(f => {
          const opts = f.options || (f.entityOptions ? Store.all(f.entityOptions, companyId).map(e=>({value:e.id,label:e.name})) : []);
          return (
            <select key={f.key} className="fsel" value={fvals[f.key]||''} onChange={e=>{setFvals(p=>({...p,[f.key]:e.target.value}));setPage(1);}}>
              <option value="">All {f.label}</option>
              {opts.map(o=><option key={o.value} value={o.value}>{o.label}</option>)}
            </select>
          );
        })}
        {(search || Object.values(fvals).some(Boolean)) && (
          <button className="btn btn-gh btn-sm" onClick={()=>{setSearch('');setFvals({});setPage(1);}}>Clear</button>
        )}
        <span className="f-cnt">{filtered.length} record{filtered.length!==1?'s':''}</span>
      </div>

      {/* Table */}
      <div className="card">
        <div className="tbl-w">
          <table className="tbl">
            <thead>
              <tr>
                {columns.map(c=><th key={c.key}>{c.label}</th>)}
                {(canEdit||canDelete||customActions) && <th>Actions</th>}
              </tr>
            </thead>
            <tbody>
              {paged.length===0 ? (
                <tr className="empty">
                  <td colSpan={columns.length+(canEdit||canDelete?1:0)}>
                    <div style={{textAlign:'center',padding:'44px 20px',color:'var(--txt2)'}}>
                      <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{margin:'0 auto 10px',display:'block',opacity:.4}}><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
                      No records found
                    </div>
                  </td>
                </tr>
              ) : paged.map(item=>(
                <tr key={item.id}>
                  {columns.map(c=><td key={c.key}>{renderCell(c,item)}</td>)}
                  {(canEdit||canDelete||customActions) && (
                    <td>
                      <div className="ra">
                        {canEdit   && <button className="btn btn-wh btn-sm" onClick={()=>openEdit(item)}>Edit</button>}
                        {canDelete && <button className="btn btn-rd btn-sm" onClick={()=>setDelId(item.id)}>Delete</button>}
                        {customActions && customActions(item)}
                      </div>
                    </td>
                  )}
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>

      {/* Pagination */}
      {totalPgs>1 && (
        <div className="pag">
          <button className="pg-b" onClick={()=>setPage(1)} disabled={page===1}>«</button>
          <button className="pg-b" onClick={()=>setPage(p=>p-1)} disabled={page===1}>‹</button>
          <span className="pg-inf">Page {page} of {totalPgs}</span>
          <button className="pg-b" onClick={()=>setPage(p=>p+1)} disabled={page===totalPgs}>›</button>
          <button className="pg-b" onClick={()=>setPage(totalPgs)} disabled={page===totalPgs}>»</button>
        </div>
      )}

      {/* Add/Edit Modal */}
      {modal==='form' && (
        <div className="mbg">
          <div className={`mod mod-${modalSize}`}>
            <div className="mod-hd">
              <h2>{editItem?'Edit':'Add'} {title.replace(/s$/, '')}</h2>
              <button className="mod-x" onClick={()=>setModal(null)}>×</button>
            </div>
            <form onSubmit={handleSave}>
              <div className="mod-bd">
                <div className="fg">
                  {fields.map(f => {
                    if (f.type==='divider') return <hr key={f.key||Math.random()} className="f-div" />;
                    if (f.type==='section') return <div key={f.key||Math.random()} className="f-sec">{f.label}</div>;
                    const opts = resolveOpts(f);
                    return (
                      <div key={f.key} className={`fld ${f.full?'full':''}`}>
                        <label>{f.label}{f.required && <span className="req">*</span>}</label>
                        {f.type==='select' ? (
                          <window.FormSelect placeholder={'Select '+f.label} value={form[f.key]||''} onChange={v=>setField(f.key, v)} options={opts}/>
                        ) : f.type==='textarea' ? (
                          <textarea className="inp tarea" value={form[f.key]||''} onChange={e=>setField(f.key,e.target.value)} required={f.required} rows={3} placeholder={f.placeholder} />
                        ) : (
                          <input className="inp" type={f.type||'text'} value={form[f.key]||''} onChange={e=>setField(f.key, e.target.value)} required={f.required} placeholder={f.placeholder} step={f.step} min={f.min} readOnly={f.readOnly} />
                        )}
                      </div>
                    );
                  })}
                </div>
              </div>
              <div className="mod-ft">
                <button type="button" className="btn btn-wh" onClick={()=>setModal(null)}>Cancel</button>
                <button type="submit" className="btn btn-or">{editItem?'Update':saveLabel||'Save'}</button>
              </div>
            </form>
          </div>
        </div>
      )}

      {/* Delete Confirm */}
      {delId && <Confirm onOk={handleDelete} onCancel={()=>setDelId(null)} />}
    </div>
  );
}
// ── Filter Panel ─────────────────────────────────────────
function FilterPanel({show,fields,values,onChange,onApply,onRefresh,onExport,onClear}) {
  if (!show) return null;
  return (
    <div className="fp">
      <div className="fp-row">
        {(fields||[]).map(f=>(
          <div key={f.key} className="fp-fld">
            <label>{f.label}</label>
            {f.type==='date'?(
              <input type="date" value={values[f.key]||''} onChange={e=>onChange(f.key,e.target.value)} style={{width:f.width||130}}/>
            ):(
              <select value={values[f.key]||''} onChange={e=>onChange(f.key,e.target.value)} style={{width:f.width||140}}>
                <option value="">All {f.label}</option>
                {(f.options||[]).map(o=><option key={o.value} value={o.value}>{o.label}</option>)}
              </select>
            )}
          </div>
        ))}
        <div className="fp-acts">
          <button className="btn btn-or btn-sm" onClick={onApply}>Apply</button>
          <div className="fp-div"/>
          <button className="btn btn-wh btn-sm" onClick={onRefresh}><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg> Refresh</button>
          {onExport&&<button className="btn btn-wh btn-sm" onClick={onExport}><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> Export</button>}
          <button className="btn btn-gh btn-sm" onClick={onClear}>Clear All</button>
        </div>
      </div>
    </div>
  );
}
window.FilterPanel = FilterPanel;
window.CRUDPage = CRUDPage;

// ── useStoreSync ──────────────────────────────────────────────────────────────
// Call once at the top of any page component that shows master-data dropdowns
// or transaction lists. Subscribes the component to ALL Store mutations so:
//   • Every Store.all() called in the render body returns live data
//   • Dropdowns, lists, and KPIs refresh the instant any record is created,
//     edited, deleted, or imported — no page reload, no manual refresh needed
// Works for any current or future entity: companies, vendors, materials, etc.
function useStoreSync() {
  var pair = React.useState(0);
  React.useEffect(function () {
    var _t = null;
    var unsub = Store.on(function () {
      clearTimeout(_t);
      _t = setTimeout(function () { pair[1](function (n) { return n + 1; }); }, 80);
    });
    return function () { clearTimeout(_t); unsub(); };
  }, []);
}
window.useStoreSync = useStoreSync;

// ── MasterEmptyNote ───────────────────────────────────────────────────────────
// Renders a yellow inline hint when a required master list is empty.
// Usage: <window.MasterEmptyNote list={vendors} noun="vendors" module="Vendors" />
function MasterEmptyNote({ list, noun, module: mod }) {
  if (!list || list.length > 0) return null;
  return (
    <div style={{display:'flex',alignItems:'center',gap:6,padding:'5px 9px',marginTop:4,
      background:'#FEF9C3',border:'1px solid #FDE047',borderRadius:'var(--r)',
      fontSize:11,color:'#713F12',lineHeight:1.5}}>
      <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#CA8A04"
        strokeWidth="2.5" style={{flexShrink:0}}>
        <circle cx="12" cy="12" r="10"/>
        <line x1="12" y1="8" x2="12" y2="12"/>
        <line x1="12" y1="16" x2="12.01" y2="16"/>
      </svg>
      No {noun} found — add one in <strong style={{marginLeft:2}}>{mod}</strong> first.
    </div>
  );
}

// ── Assignment Engine ─────────────────────────────────────────────────────────
// Centralised framework controlling which companies can access master records.
// Works for: Materials · Customers · Vendors · Crushers · Transport Rates
//
// Record assignment fields (stored on each master record):
//   allCompanies: true          → visible to every company (default / backward-compat)
//   allCompanies: false
//   assignedCompanies: ['id',…] → visible only to those companies
//
// Utility: isAssignedTo(record, companyId) ─ returns boolean
function isAssignedTo(record, companyId) {
  if (!record) return false;
  if (!companyId || companyId === 'group') return true; // group view sees everything
  // If record explicitly restricts to specific companies, check the list
  if (record.allCompanies === false && Array.isArray(record.assignedCompanies)) {
    return record.assignedCompanies.includes(companyId);
  }
  return true; // default: available to all (backward-compat for records without assignment data)
}
window.isAssignedTo = isAssignedTo;

// Utility: filterAssigned(records, companyId) ─ post-filter any Store.all() result
function filterAssigned(records, companyId) {
  if (!companyId || companyId === 'group') return records || [];
  return (records || []).filter(function(r) { return isAssignedTo(r, companyId); });
}
window.filterAssigned = filterAssigned;

// ── CompanyAssignmentSection ──────────────────────────────────────────────────
// Drop-in form section. Place anywhere inside a modal mod-bd.
// Props:
//   form     — current form state object (reads allCompanies / assignedCompanies)
//   setForm  — React state setter: fn(prev => ({...prev, ...}))
function CompanyAssignmentSection({ form, setForm }) {
  const companies = Store.all('companies');
  const isAll = form.allCompanies !== false; // default true → "All Companies" ticked

  function handleToggleAll(e) {
    if (e.target.checked) {
      setForm(function(p) { return Object.assign({}, p, { allCompanies: true,  assignedCompanies: [] }); });
    } else {
      setForm(function(p) { return Object.assign({}, p, { allCompanies: false, assignedCompanies: [] }); });
    }
  }

  function handleToggleCo(coId, checked) {
    var cur = form.assignedCompanies || [];
    var next = checked ? [...cur, coId] : cur.filter(function(id) { return id !== coId; });
    setForm(function(p) { return Object.assign({}, p, { assignedCompanies: next }); });
  }

  var assigned = form.assignedCompanies || [];

  return (
    <div style={{marginTop:16}}>
      <div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:10,paddingBottom:7,borderBottom:'2px solid #FEF3E8'}}>
        <span style={{color:'var(--or)',fontWeight:700,fontSize:13}}>Availability &amp; Company Assignment</span>
        {!isAll && assigned.length > 0 && (
          <span style={{fontSize:11,color:'var(--ok)',fontWeight:600,display:'flex',alignItems:'center',gap:4}}>
            <span style={{width:6,height:6,borderRadius:'50%',background:'var(--ok)',display:'inline-block'}}></span>
            {assigned.length} compan{assigned.length===1?'y':'ies'} assigned
          </span>
        )}
      </div>

      {/* All Companies toggle */}
      <label style={{display:'flex',alignItems:'flex-start',gap:9,cursor:'pointer',padding:'9px 11px',borderRadius:'var(--r)',border:`1px solid ${isAll?'var(--or-bdr)':'var(--bdr)'}`,background:isAll?'var(--or-lt)':'#fff',marginBottom:8,transition:'all .1s'}}>
        <input type="checkbox" checked={isAll} onChange={handleToggleAll} style={{accentColor:'var(--or)',marginTop:1,flexShrink:0}}/>
        <div>
          <div style={{fontWeight:600,fontSize:12,color:isAll?'var(--or)':'var(--txt)'}}>Use Across All Companies</div>
          <div style={{fontSize:11,color:'var(--txt2)',marginTop:2,lineHeight:1.5}}>
            This record is available to every company in the group. Any new company added later automatically inherits access.
          </div>
        </div>
      </label>

      {/* Per-company list (only shown when "All" is unchecked) */}
      {!isAll && (
        <div>
          <div style={{fontSize:10.5,fontWeight:700,color:'var(--txt2)',textTransform:'uppercase',letterSpacing:'.04em',marginBottom:6}}>
            Assign To Specific Companies
          </div>
          {companies.length === 0
            ? <div style={{fontSize:11.5,color:'var(--txt3)',fontStyle:'italic',padding:'8px 0'}}>No companies found</div>
            : <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:5}}>
                {companies.map(function(co) {
                  var checked = assigned.includes(co.id);
                  return (
                    <label key={co.id} style={{display:'flex',alignItems:'center',gap:7,cursor:'pointer',padding:'6px 9px',borderRadius:'var(--r)',border:`1px solid ${checked?'var(--or-bdr)':'var(--bdr)'}`,background:checked?'var(--or-lt)':'#fff',fontSize:12,transition:'all .1s',userSelect:'none'}}>
                      <input type="checkbox" checked={checked} onChange={function(e){handleToggleCo(co.id,e.target.checked);}} style={{accentColor:'var(--or)',flexShrink:0}}/>
                      <span style={{fontWeight:checked?600:400,color:checked?'var(--or)':'var(--txt)'}}>{co.name}</span>
                    </label>
                  );
                })}
              </div>
          }
          {assigned.length === 0 && companies.length > 0 && (
            <div style={{marginTop:7,fontSize:11,color:'var(--warn)',display:'flex',alignItems:'center',gap:5}}>
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{display:'inline',verticalAlign:'-1px',flexShrink:0,marginRight:6}}><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg> No companies selected — this record will not be visible in any company.
            </div>
          )}
        </div>
      )}
    </div>
  );
}
window.CompanyAssignmentSection = CompanyAssignmentSection;
window.MasterEmptyNote = MasterEmptyNote;
