// Screens: Rate List, Rate Detail/Edit, Add Rate, Import Wizard

const RATE_STATUSES = ["Active", "Draft", "Inactive"];

// ─── All possible table columns (mirrors export CSV) ─────────────────────────
const ALL_COLUMNS = [
  { key: "id",                 label: "Rate ID",          cls: "mono tiny" },
  { key: "terminal",           label: "Terminal" },
  { key: "terminal_code",      label: "Terminal Code",    cls: "mono tiny muted" },
  { key: "terminal_name",      label: "Terminal Name",    cls: "muted" },
  { key: "cost_category",      label: "Cost Category",    cls: "muted" },
  { key: "cost_type",          label: "Cost Type" },
  { key: "cost_type_name",     label: "Cost Type Name",   cls: "muted" },
  { key: "sap_material_code",  label: "SAP Material",     cls: "mono tiny muted" },
  { key: "material_name",      label: "Material",         cls: "tiny muted" },
  { key: "charge_unit_code",   label: "UOM",              cls: "muted" },
  { key: "containertype_code", label: "Ctr Type",         cls: "muted" },
  { key: "container_size",     label: "Size",             cls: "muted" },
  { key: "container_fullness", label: "Fullness",         cls: "muted" },
  { key: "shipment_type_code", label: "Shipment",         cls: "muted" },
  { key: "rate_local",         label: "Rate (Local)",     cls: "num bold" },
  { key: "iso_code",           label: "Currency",         cls: "muted" },
  { key: "rate_usd",           label: "≈ USD",            cls: "num muted" },
  { key: "effective_date",     label: "Effective",        cls: "mono tiny" },
  { key: "expiry_date",        label: "Expires",          cls: "mono tiny" },
  { key: "slab_unit_code",     label: "Slab Unit",        cls: "muted" },
  { key: "slab_type_code",     label: "Slab Type",        cls: "muted" },
  { key: "slab_min",           label: "Slab Min",         cls: "num muted" },
  { key: "slab_max",           label: "Slab Max",         cls: "num muted" },
  { key: "sap_vendor_code",    label: "SAP Vendor",       cls: "mono tiny muted" },
  { key: "vendor_name",        label: "Vendor Name",      cls: "muted" },
  { key: "contract_ref",       label: "Contract",         cls: "mono tiny muted" },
  { key: "contract_owner",     label: "Owner",            cls: "muted" },
  { key: "transport_type",     label: "Transport",        cls: "muted" },
  { key: "route",              label: "Route",            cls: "muted" },
  { key: "corridor_from",      label: "Corridor From",    cls: "muted" },
  { key: "corridor_to",        label: "Corridor To",      cls: "muted" },
  { key: "payment_terms",      label: "Payment Terms",    cls: "muted" },
  { key: "brand_company_name", label: "Brand Company",    cls: "muted" },
  { key: "payable_by",         label: "Payable By",       cls: "muted" },
  { key: "notes",              label: "Notes",            cls: "tiny muted" },
  { key: "is_invoicerate",     label: "Invoice Rate",     cls: "center" },
  { key: "stcy_flag",          label: "STCY",             cls: "center" },
  { key: "calculator",         label: "Calculator",       cls: "center" },
  { key: "source",             label: "Source",           cls: "tiny muted" },
  { key: "status",             label: "Status" },
  { key: "created_by",         label: "Created By",       cls: "tiny muted" },
  { key: "created_at",         label: "Created At",       cls: "mono tiny muted" },
  { key: "modified_by",        label: "Updated By",       cls: "tiny muted" },
  { key: "modified_at",        label: "Updated At",       cls: "mono tiny muted" },
];

const DEFAULT_VIEW_COLUMNS = ["terminal","cost_type","charge_unit_code","rate_local","rate_usd","effective_date","contract_ref","status","modified_by"];

function renderCell(col, r, roeMap) {
  switch (col.key) {
    case "id":                return r.id;
    case "terminal":          return <><div className="bold">{r.terminal_name}</div><div className="tiny muted mono">{r.terminal_code}</div></>;
    case "terminal_code":     return r.terminal_code;
    case "terminal_name":     return r.terminal_name;
    case "cost_category":     return r.cost_category;
    case "cost_type":         return <><div>{r.cost_type_name}</div>{r.material_name && <div className="tiny muted">{r.material_name}</div>}</>;
    case "cost_type_name":    return r.cost_type_name;
    case "sap_material_code": return r.sap_material_code;
    case "material_name":     return r.material_name;
    case "charge_unit_code":  return r.charge_unit_code;
    case "containertype_code":return r.containertype_code;
    case "container_size":    return r.container_size;
    case "container_fullness":return r.container_fullness;
    case "shipment_type_code":return r.shipment_type_code;
    case "rate_local":        return <>{fmt.num(r.rate_local, 2)} <span className="tiny muted">{r.iso_code}</span></>;
    case "iso_code":          return r.iso_code;
    case "rate_usd":          return fmt.usd(r.rate_local / (roeMap[r.iso_code] || 1));
    case "effective_date":    return r.effective_date?.slice(0, 10);
    case "expiry_date":       return r.expiry_date?.slice(0, 10);
    case "slab_unit_code":    return r.slab_unit_code;
    case "slab_type_code":    return r.slab_type_code;
    case "slab_min":          return r.slab_min;
    case "slab_max":          return r.slab_max;
    case "sap_vendor_code":   return r.sap_vendor_code;
    case "vendor_name":       return r.vendor_name;
    case "contract_ref":      return r.contract_ref;
    case "contract_owner":    return r.contract_owner;
    case "transport_type":    return r.transport_type;
    case "route":             return r.route;
    case "route_rate":        return r.route_rate;
    case "corridor_from":     return r.corridor_from;
    case "corridor_to":       return r.corridor_to;
    case "payment_terms":     return r.payment_terms;
    case "brand_company_name": return r.brand_company_name || r.brand_company_code;
    case "payable_by":        return r.payable_by;
    case "notes":             return r.notes;
    case "is_invoicerate":    return r.is_invoicerate ? "✓" : "";
    case "stcy_flag":         return r.stcy_flag ? "✓" : "";
    case "calculator":        return r.calculator ? "✓" : "";
    case "source":            return r.source;
    case "status":            return <Pill status={r.status} />;
    case "created_by":        return r.created_by;
    case "created_at":        return r.created_at?.slice(0, 10);
    case "modified_by":       return <><span>{r.modified_by}</span><br/><span className="mono">{r.modified_at?.slice(5,10)}</span></>;
    case "modified_at":       return r.modified_at?.slice(0, 10);
    default:                  return r[col.key] ?? "";
  }
}

// ─── Manage Views Modal ───────────────────────────────────────────────────────
function ManageViewsModal({ views, currentFilters, onClose, onApply, onSaved, user, perms, initialViewId }) {
  const isAdmin = (user?.roles || []).map(r => r.toLowerCase()).includes("admin");
  const [selected, setSelected] = React.useState(
    views.find(v => v.id === initialViewId) || views.find(v => v.is_default) || views[0] || null
  );
  const [editName, setEditName] = React.useState("");
  const [editCols, setEditCols] = React.useState([]);
  const [editFilters, setEditFilters] = React.useState({});
  const [saving, setSaving] = React.useState(false);
  const [err, setErr] = React.useState(null);
  const [availSearch, setAvailSearch] = React.useState("");
  const [availSort, setAvailSort] = React.useState("default"); // "default" | "az"
  const colListRef = React.useRef();
  const dragFromKey = React.useRef(null);
  const [dragOverKey, setDragOverKey] = React.useState(null);
  const [filtersCaptured, setFiltersCaptured] = React.useState(false);

  React.useEffect(() => {
    if (selected) {
      setEditName(selected.name);
      setEditCols(Array.isArray(selected.columns) ? [...selected.columns] : [...DEFAULT_VIEW_COLUMNS]);
      setEditFilters(selected.filters || {});
    }
  }, [selected?.id]);

  const canEdit = selected && (isAdmin || (!selected.is_default && (selected.owner_upn?.toLowerCase() === user?.email?.toLowerCase() || !selected.owner_upn)));

  const availableCols = React.useMemo(() => {
    let cols = ALL_COLUMNS.filter(c => !editCols.includes(c.key));
    if (availSearch.trim()) {
      const q = availSearch.toLowerCase();
      cols = cols.filter(c => c.label.toLowerCase().includes(q) || c.key.toLowerCase().includes(q));
    }
    if (availSort === "az") cols = [...cols].sort((a, b) => a.label.localeCompare(b.label));
    return cols;
  }, [editCols, availSearch, availSort]);
  const selectedColDefs = editCols.map(k => ALL_COLUMNS.find(c => c.key === k)).filter(Boolean);

  const addCol = (key) => {
    setEditCols(prev => [...prev, key]);
    // scroll to bottom so newly added item is visible
    setTimeout(() => {
      if (colListRef.current) colListRef.current.scrollTop = colListRef.current.scrollHeight;
    }, 30);
  };
  const removeCol = (key) => setEditCols(prev => prev.filter(k => k !== key));
  const moveCol = (key, dir) => {
    setEditCols(prev => {
      const next = [...prev];
      const idx = next.indexOf(key);
      if (idx === -1) return prev;
      const target = idx + dir;
      if (target < 0 || target >= next.length) return prev;
      [next[idx], next[target]] = [next[target], next[idx]];
      return next;
    });
  };

  const onDragStart = (e, key) => {
    dragFromKey.current = key;
    e.dataTransfer.effectAllowed = "move";
  };
  const onDragEnter = (e, key) => {
    e.preventDefault();
    if (dragFromKey.current && dragFromKey.current !== key) setDragOverKey(key);
  };
  const onDragOver = (e) => {
    e.preventDefault();
    // auto-scroll the list container
    const el = colListRef.current;
    if (!el) return;
    const { top, bottom } = el.getBoundingClientRect();
    const zone = 36;
    if (e.clientY < top + zone)    el.scrollTop -= 6;
    if (e.clientY > bottom - zone) el.scrollTop += 6;
  };
  const onDrop = (e, toKey) => {
    e.preventDefault();
    const fromKey = dragFromKey.current;
    if (!fromKey || fromKey === toKey) { dragFromKey.current = null; setDragOverKey(null); return; }
    setEditCols(prev => {
      const next = [...prev];
      const fromIdx = next.indexOf(fromKey);
      const toIdx   = next.indexOf(toKey);
      if (fromIdx === -1 || toIdx === -1) return prev;
      next.splice(fromIdx, 1);
      next.splice(toIdx, 0, fromKey);
      return next;
    });
    dragFromKey.current = null;
    setDragOverKey(null);
  };
  const onDragEnd = () => { dragFromKey.current = null; setDragOverKey(null); };

  const handleSave = async () => {
    if (!editName.trim()) return;
    setSaving(true); setErr(null);
    try {
      const body = { name: editName.trim(), filters: editFilters, columns: editCols };
      let res;
      if (selected.id < 0) {
        res = await apiFetch("/api/rate-views", { method: "POST", body: JSON.stringify(body) });
      } else {
        res = await apiFetch(`/api/rate-views/${selected.id}`, { method: "PUT", body: JSON.stringify(body) });
      }
      await onSaved();
    } catch(e) { setErr(e.message || "Save failed"); }
    setSaving(false);
  };

  const handleDelete = async () => {
    if (!selected || selected.is_default) return;
    if (!confirm(`Delete view "${selected.name}"?`)) return;
    setSaving(true);
    try {
      await apiFetch(`/api/rate-views/${selected.id}`, { method: "DELETE" });
      await onSaved();
      setSelected(null);
    } catch(e) { setErr(e.message || "Delete failed"); }
    setSaving(false);
  };

  const handleNew = () => {
    const baseCols = selected && Array.isArray(selected.columns) && selected.columns.length > 0
      ? [...selected.columns]
      : [...DEFAULT_VIEW_COLUMNS];
    const newView = { id: -Date.now(), name: "New View", is_default: false, owner_upn: user?.email, filters: currentFilters, columns: baseCols };
    setSelected(newView);
  };

  const handleApply = () => {
    if (!selected) return;
    onApply(selected);
    onClose();
  };

  return (
    <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.35)", zIndex: 1000, display: "flex", alignItems: "center", justifyContent: "center" }}
      onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div style={{ background: "#fff", borderRadius: 10, boxShadow: "0 8px 40px rgba(0,0,0,0.18)", width: 820, maxWidth: "95vw", maxHeight: "90vh", display: "flex", flexDirection: "column", overflow: "hidden" }}>
        <div style={{ padding: "14px 20px", borderBottom: "1px solid var(--line)", display: "flex", alignItems: "center", justifyContent: "space-between" }}>
          <span style={{ fontWeight: 700, fontSize: 15 }}>Manage Views</span>
          <button className="icon-btn" onClick={onClose} style={{ fontSize: 18, lineHeight: 1 }}>×</button>
        </div>
      <div style={{ display: "flex", gap: 0, minHeight: 420, flex: 1, overflow: "hidden" }}>
        {/* Left: view list */}
        <div style={{ width: 210, borderRight: "1px solid var(--line)", padding: "8px 0", flexShrink: 0 }}>
          {views.map(v => (
            <div key={v.id} onClick={() => setSelected(v)}
              style={{ padding: "8px 14px", cursor: "pointer", borderRadius: 0,
                background: selected?.id === v.id ? "var(--paper)" : "",
                borderLeft: selected?.id === v.id ? "3px solid var(--teal)" : "3px solid transparent",
                fontWeight: v.is_default ? 600 : 400, fontSize: 13 }}>
              {v.is_default && <span style={{ fontSize: 10, color: "var(--teal)", marginRight: 6, textTransform: "uppercase", fontWeight: 700 }}>DEFAULT</span>}
              {v.name}
              {!v.owner_upn && !v.is_default && <span className="tiny muted" style={{ marginLeft: 4 }}>(global)</span>}
            </div>
          ))}
          <div style={{ padding: "10px 14px" }}>
            <button className="btn ghost" style={{ width: "100%", fontSize: 12 }} onClick={handleNew}>
              <Icon name="plus" size={12} /> New view
            </button>
          </div>
        </div>

        {/* Right: editor */}
        {selected ? (
          <div style={{ flex: 1, padding: "16px 20px", display: "flex", flexDirection: "column", gap: 14 }}>
            <div style={{ display: "flex", gap: 10, alignItems: "center" }}>
              <input className="input" style={{ flex: 1, fontWeight: 600 }}
                value={editName} onChange={e => setEditName(e.target.value)}
                disabled={!canEdit} placeholder="View name" />
              <button className="btn ghost" style={{ fontSize: 12, whiteSpace: "nowrap" }}
                onClick={() => { setEditFilters({ ...currentFilters }); setFiltersCaptured(true); setTimeout(() => setFiltersCaptured(false), 2000); }}
                disabled={!canEdit}>
                Capture current filters{filtersCaptured && <span style={{ color: "#16a34a", marginLeft: 5 }}>✔</span>}
              </button>
            </div>

            {/* Column picker */}
            <div style={{ fontSize: 12, fontWeight: 600, color: "var(--ink-mute)", marginBottom: -6 }}>Columns</div>
            <div style={{ display: "flex", gap: 10, flex: 1 }}>
              {/* Available */}
              <div style={{ flex: 1, border: "1px solid var(--line)", borderRadius: 6, overflow: "hidden", display: "flex", flexDirection: "column" }}>
                <div style={{ padding: "6px 8px", background: "var(--paper)", borderBottom: "1px solid var(--line)", display: "flex", alignItems: "center", gap: 6 }}>
                  <span style={{ fontSize: 11, fontWeight: 600, color: "var(--ink-mute)", flexShrink: 0 }}>Available</span>
                  <input
                    value={availSearch}
                    onChange={e => setAvailSearch(e.target.value)}
                    placeholder="Search…"
                    style={{ flex: 1, fontSize: 11, border: "1px solid var(--line)", borderRadius: 4, padding: "2px 6px", outline: "none", minWidth: 0 }}
                  />
                  <button
                    title={availSort === "az" ? "Sort: A–Z (click for default)" : "Sort: Default (click for A–Z)"}
                    onClick={() => setAvailSort(s => s === "az" ? "default" : "az")}
                    style={{ fontSize: 10, padding: "2px 6px", border: "1px solid var(--line)", borderRadius: 4,
                      background: availSort === "az" ? "var(--teal-light, #e6f6f9)" : "#fff",
                      color: availSort === "az" ? "var(--teal)" : "var(--ink-mute)",
                      cursor: "pointer", flexShrink: 0, fontWeight: 600 }}>
                    A–Z
                  </button>
                </div>
                <div style={{ maxHeight: 260, overflowY: "auto", flex: 1 }}>
                  {availableCols.map(c => (
                    <div key={c.key} onClick={() => canEdit && addCol(c.key)}
                      style={{ padding: "6px 10px", fontSize: 13, cursor: canEdit ? "pointer" : "default",
                        display: "flex", alignItems: "center", justifyContent: "space-between",
                        borderBottom: "1px solid var(--line, #eee)" }}
                      onMouseEnter={e => e.currentTarget.style.background = "var(--paper)"}
                      onMouseLeave={e => e.currentTarget.style.background = ""}>
                      <span>{c.label}</span>
                      {canEdit && <span style={{ color: "var(--teal)", fontSize: 16, lineHeight: 1, paddingLeft: 8 }}>+</span>}
                    </div>
                  ))}
                  {availableCols.length === 0 && (
                    <div className="tiny muted" style={{ padding: "12px 10px" }}>
                      {availSearch ? "No matching fields" : "All columns selected"}
                    </div>
                  )}
                </div>
              </div>

              {/* Selected (arrow reorder) */}
              <div style={{ flex: 1, border: "1px solid var(--line)", borderRadius: 6, overflow: "hidden" }}>
                <div style={{ padding: "6px 8px", background: "var(--paper)", borderBottom: "1px solid var(--line)", display: "flex", alignItems: "center", gap: 6 }}>
                  <span style={{ fontSize: 11, fontWeight: 600, color: "var(--ink-mute)", flexShrink: 0 }}>Displayed</span>
                  <span style={{ fontSize: 11, color: "var(--ink-mute)" }}>({selectedColDefs.length})</span>
                  {selectedColDefs.length > 0 && canEdit && (
                    <button onClick={() => setEditCols([])}
                      style={{ marginLeft: "auto", fontSize: 10, padding: "1px 6px", border: "1px solid var(--line)", borderRadius: 4,
                        background: "#fff", color: "var(--ink-mute)", cursor: "pointer" }}>
                      Clear all
                    </button>
                  )}
                </div>
                <div ref={colListRef} style={{ maxHeight: 260, overflowY: "auto" }} onDragOver={onDragOver}>
                  {selectedColDefs.map((c, idx) => (
                    <div key={c.key}
                      draggable={canEdit}
                      onDragStart={e => onDragStart(e, c.key)}
                      onDragEnter={e => onDragEnter(e, c.key)}
                      onDrop={e => onDrop(e, c.key)}
                      onDragEnd={onDragEnd}
                      style={{
                        padding: "4px 8px 4px 6px", fontSize: 13,
                        display: "flex", alignItems: "center", gap: 6,
                        borderBottom: "1px solid var(--line, #eee)",
                        borderTop: dragOverKey === c.key ? "2px solid var(--teal, #009eb4)" : "2px solid transparent",
                        background: dragOverKey === c.key ? "var(--teal-light, #e6f6f9)" : "",
                        cursor: canEdit ? "grab" : "default",
                        userSelect: "none",
                      }}>
                      {canEdit && <span style={{ color: "var(--ink-mute)", fontSize: 14, flexShrink: 0 }}>⠿</span>}
                      <span style={{ flex: 1 }}>{c.label}</span>
                      {canEdit && (
                        <div style={{ display: "flex", gap: 1, flexShrink: 0 }}>
                          <button className="icon-btn" title="Move up" disabled={idx === 0}
                            onClick={() => moveCol(c.key, -1)}
                            style={{ width: 20, height: 20, fontSize: 11, opacity: idx === 0 ? 0.3 : 1 }}>↑</button>
                          <button className="icon-btn" title="Move down" disabled={idx === selectedColDefs.length - 1}
                            onClick={() => moveCol(c.key, 1)}
                            style={{ width: 20, height: 20, fontSize: 11, opacity: idx === selectedColDefs.length - 1 ? 0.3 : 1 }}>↓</button>
                          <button className="icon-btn" title="Remove" onClick={() => removeCol(c.key)}
                            style={{ width: 20, height: 20, fontSize: 14, color: "var(--ink-mute)" }}>×</button>
                        </div>
                      )}
                    </div>
                  ))}
                  {selectedColDefs.length === 0 && <div className="tiny muted" style={{ padding: "12px 10px" }}>No columns selected. Click + in Available to add.</div>}
                </div>
              </div>
            </div>

            {err && <div className="tiny" style={{ color: "var(--red)" }}>{err}</div>}

            <div style={{ display: "flex", gap: 8, paddingTop: 4 }}>
              {selected.id > 0 && (
                <button className="btn primary" style={{ minWidth: 80 }} onClick={handleApply}>Apply</button>
              )}
              {canEdit && (
                <button className="btn primary" style={{ minWidth: 80 }} onClick={handleSave} disabled={saving}>
                  {saving ? "Saving…" : selected.id < 0 ? "Create" : "Save"}
                </button>
              )}
              {canEdit && !selected.is_default && selected.id > 0 && (
                <button className="btn ghost" style={{ color: "var(--red)" }} onClick={handleDelete} disabled={saving}>Delete</button>
              )}
              <button className="btn ghost" style={{ marginLeft: "auto" }} onClick={onClose}>Close</button>
            </div>
          </div>
        ) : (
          <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", color: "var(--ink-mute)", fontSize: 13 }}>
            Select a view or create a new one
          </div>
        )}
      </div>
      </div>
    </div>
  );
}

function RateList({ user, perms, onOpenRate, onAdd, onAddPrefill, onImport, onOpenAI, refreshKey, onToast }) {
  const [filters, setFilters] = useState({
    terminals: [],
    costTypes: [],
    statuses: ["Active"],
    containerTypes: [],
    containerSizes: [],
    containerFullnesses: [],
    invoiceRate: false,
    stcyFlag: false,
    calculator: false,
    q: "",
  });
  const [page, setPage] = useState(1);
  const [sortKey, setSortKey] = useState(null);
  const [sortDir, setSortDir] = useState("asc");
  const [colFilters, setColFilters] = useState({});
  const [rows, setRows] = useState([]);
  const [loading, setLoading] = useState(true);
  const [terminals, setTerminals] = useState([]);
  const [costTypes, setCostTypes] = useState([]);
  const [statusOptions, setStatusOptions] = useState([]);
  const [equipTypes, setEquipTypes] = useState([]);
  const [roeMap, setRoeMap] = useState({});
  const [selectedIds, setSelectedIds] = useState(new Set());
  const [selectAllPages, setSelectAllPages] = useState(false);
  const [showBatchEdit, setShowBatchEdit] = useState(false);
  const [savedViews, setSavedViews] = useState([]);
  const [activeView, setActiveView] = useState(null);
  const [manageViewsInitId, setManageViewsInitId] = useState(null);
  const openManageViews = () => { setManageViewsInitId(activeView?.id ?? null); };
  const [viewMenuOpen, setViewMenuOpen] = useState(false);
  const viewMenuRef = useRef();
  const [groupBySlab, setGroupBySlab] = useState(false);
  const [expandedGroups, setExpandedGroups] = useState(new Set());
  const [filterMultiGroup, setFilterMultiGroup] = useState(false);
  const pageSize = 14;

  const activeColumns = useMemo(() => {
    const cols = activeView?.columns;
    return Array.isArray(cols) && cols.length > 0 ? cols : DEFAULT_VIEW_COLUMNS;
  }, [activeView]);

  const loadViews = () => apiFetch("/api/rate-views").then(data => {
    if (!Array.isArray(data)) return;
    setSavedViews(data);
    setActiveView(prev => {
      if (prev) return data.find(v => v.id === prev.id) || prev;
      return data.find(v => v.is_default) || data[0] || null;
    });
  }).catch(() => {});

  const load = () => {
    setLoading(true);
    const params = new URLSearchParams();
    filters.terminals.forEach(id => params.append("terminal_id", id));
    filters.costTypes.forEach(id => params.append("cost_type_id", id));
    filters.statuses.forEach(s => params.append("status", s));
    filters.containerTypes.forEach(id => params.append("containertype", id));
    filters.containerSizes.forEach(s => params.append("container_size", s));
    filters.containerFullnesses.forEach(s => params.append("container_fullness", s));
    if (filters.invoiceRate) params.append("is_invoicerate", "true");
    if (filters.stcyFlag) params.append("stcy_flag", "true");
    if (filters.calculator) params.append("calculator", "true");
    Promise.all([
      apiFetch(`/api/rates?${params}&limit=500`),
      apiFetch("/api/metadata/terminals/scoped"),
      apiFetchMeta("cost-types"),
      apiFetch("/api/roe/latest"),
      apiFetchMeta("rate-statuses"),
      apiFetchMeta("equipment-types"),
    ]).then(([rates, terms, cts, roe, statuses, equips]) => {
      const roe_by_iso = {};
      (Array.isArray(roe) ? roe : []).forEach(r => { roe_by_iso[r.iso_code] = r.rate; });
      setRoeMap(roe_by_iso);
      setTerminals(Array.isArray(terms) ? terms : []);
      setCostTypes(Array.isArray(cts) ? cts : []);
      setStatusOptions(Array.isArray(statuses) ? statuses.filter(s => s.is_active) : []);
      setEquipTypes(Array.isArray(equips) ? equips.filter(e => e.is_active) : []);
      const rateArr = Array.isArray(rates) ? rates : [];
      setRows(rateArr);
      setLoading(false);
    }).catch(() => setLoading(false));
  };

  // Always keep a fresh ref so the refreshKey effect never closes over a stale load
  const loadRef = useRef(load);
  loadRef.current = load;

  useEffect(() => { loadViews(); }, []);

  useEffect(() => { load(); setPage(1); }, [
    filters.terminals.join(","), filters.costTypes.join(","), filters.statuses.join(","),
    filters.containerTypes.join(","), filters.containerSizes.join(","), filters.containerFullnesses.join(","),
    filters.invoiceRate, filters.stcyFlag, filters.calculator,
  ]);
  useEffect(() => { if (refreshKey > 0) { loadRef.current(); setPage(1); } }, [refreshKey]);

  const handleSupersede = async (r) => {
    const isBatch = effectiveSelectedCount > 1;
    const msg = isBatch
      ? `Deactivate ${effectiveSelectedCount} selected rates?\n\nAll selected records will be marked as Inactive. This cannot be undone.`
      : `Deactivate rate #${r.id}?\n\nThis will mark it as Inactive and cannot be undone.`;
    if (!confirm(msg)) return;
    try {
      if (isBatch) {
        const res = await apiFetch(`${API_BASE}/api/rates/batch`, {
          method: "PATCH",
          body: JSON.stringify({ ids: effectiveSelectedIds, updates: { status: "Inactive" } }),
        });
        clearSelection();
        alert(`Deactivated ${res.updated} rate(s).`);
      } else {
        await apiFetch(`${API_BASE}/api/rates/${r.id}`, { method: "DELETE" });
      }
      load();
    } catch (e) {
      alert("Failed to deactivate: " + (e.message || e));
    }
  };

  const handleBulkDuplicate = async () => {
    const ids = effectiveSelectedIds;
    const count = ids.length;
    if (!confirm(`Duplicate ${count} record${count !== 1 ? "s" : ""} as Draft? They will be saved directly to the database.`)) return;
    const selectedRows = rows.filter(r => ids.includes(r.id));
    let ok = 0, fail = 0;
    await Promise.all(selectedRows.map(async r => {
      const body = {
        terminal_id: r.terminal_id,
        cost_type_id: r.cost_type_id,
        charge_unit_id: r.charge_unit_id,
        containertype: r.containertype ?? r.cargo_type_id,
        shipment_type: r.shipment_type,
        primary_currency: r.primary_currency,
        rate_local: r.rate_local,
        route_rate: r.route_rate ?? null,
        slab_min: r.slab_min ?? null,
        slab_max: r.slab_max ?? null,
        slab_unit: r.slab_unit ?? null,
        slab_type: r.slab_type ?? null,
        effective_date: r.effective_date,
        expiry_date: r.expiry_date || null,
        material_name: r.material_name,
        contract_material_name: r.contract_material_name,
        sap_material_code: r.sap_material_code,
        sap_vendor_code: r.sap_vendor_code,
        contract_ref: r.contract_ref,
        contract_owner: r.contract_owner,
        payment_terms: r.payment_terms,
        notes: r.notes,
        transport_type: r.transport_type,
        secondary_currency: r.secondary_currency,
        route: r.route,
        corridor_from: r.corridor_from,
        corridor_to: r.corridor_to,
        container_size: r.container_size,
        container_fullness: r.container_fullness,
        cost_category: r.cost_category,
        is_invoicerate: r.is_invoicerate,
        stcy_flag: r.stcy_flag,
        calculator: r.calculator,
        status: "Draft",
        source: "Manual",
      };
      try {
        await apiFetch("/api/rates", { method: "POST", body: JSON.stringify(body) });
        ok++;
      } catch (_) { fail++; }
    }));
    clearSelection();
    load();
    if (onToast) onToast(`Duplicated ${ok} rate${ok !== 1 ? "s" : ""} as Draft${fail > 0 ? ` (${fail} failed)` : ""}.`);
  };

  const handleDuplicate = (r) => {
    if (effectiveSelectedCount > 1 && (selectAllPages || selectedIds.has(r.id))) {
      handleBulkDuplicate();
      return;
    }
    const prefill = { ...r, id: undefined, status: "Draft", created_at: undefined, modified_at: undefined };
    if (onAddPrefill) onAddPrefill(prefill);
  };

  const handleDelete = async (r) => {
    const isBatch = effectiveSelectedCount > 1;
    const msg = isBatch
      ? `Permanently delete ${effectiveSelectedCount} selected rates?\n\nOnly Draft records will be deleted. Non-Draft records will be skipped.\n\nThis action cannot be undone.`
      : `Permanently delete rate #${r.id}?\n\nThis action cannot be undone.`;
    if (!confirm(msg)) return;
    try {
      if (isBatch) {
        const res = await apiFetch(`${API_BASE}/api/rates/batch-delete`, {
          method: "POST",
          body: JSON.stringify({ ids: effectiveSelectedIds }),
        });
        clearSelection();
        alert(`Deleted ${res.deleted} rate(s)${res.skipped > 0 ? `, skipped ${res.skipped} non-Draft record(s)` : ""}.`);
      } else {
        await apiFetch(`${API_BASE}/api/rates/${r.id}/hard`, { method: "DELETE" });
      }
      load();
    } catch (e) {
      alert("Failed to delete: " + (e.message || e));
    }
  };

  useEffect(() => {
    const h = (e) => { if (viewMenuRef.current && !viewMenuRef.current.contains(e.target)) setViewMenuOpen(false); };
    document.addEventListener("mousedown", h);
    return () => document.removeEventListener("mousedown", h);
  }, []);
  useEffect(() => {
    if (!loading) {
      const q = filters.q.toLowerCase();
      // client-side q filter only (server already filtered by other dims)
    }
  }, [filters.q]);

  const NUMERIC_KEYS = new Set(["rate_local", "rate_usd", "route_rate", "slab_min", "slab_max"]);
  const BOOL_KEYS   = new Set(["is_invoicerate", "stcy_flag", "calculator"]);

  function handleSort(key) {
    if (sortKey === key) {
      setSortDir(d => d === "asc" ? "desc" : "asc");
    } else {
      setSortKey(key);
      setSortDir("asc");
    }
    setPage(1);
  }

  const activeColFilters = useMemo(() => {
    return Object.fromEntries(
      Object.entries(colFilters).filter(([k, v]) => Array.isArray(v) && v.length > 0 && activeColumns.includes(k))
    );
  }, [colFilters, activeColumns]);

  const displayRows = useMemo(() => {
    const q = filters.q?.trim().toLowerCase();
    const searchKeys = activeColumns.filter(k => !NUMERIC_KEYS.has(k) && !BOOL_KEYS.has(k));
    let result = rows;
    if (q) {
      const terms = q.split(/\s+/).filter(Boolean);
      result = result.filter(r => {
        const haystack = searchKeys.map(k => {
          const v = r[k];
          return v == null ? "" : String(v);
        }).join(" ").toLowerCase();
        return terms.every(t => haystack.includes(t));
      });
    }
    const cfEntries = Object.entries(activeColFilters);
    if (cfEntries.length > 0) {
      result = result.filter(r =>
        cfEntries.every(([k, allowed]) => {
          const rv = r[k];
          const s = rv == null ? "" : String(rv);
          return allowed.includes(s);
        })
      );
    }
    if (sortKey) {
      const dir = sortDir === "asc" ? 1 : -1;
      const isNum = NUMERIC_KEYS.has(sortKey);
      result = [...result].sort((a, b) => {
        const av = a[sortKey] ?? (isNum ? -Infinity : "");
        const bv = b[sortKey] ?? (isNum ? -Infinity : "");
        if (isNum) return (av - bv) * dir;
        return String(av).localeCompare(String(bv)) * dir;
      });
    }
    return result;
  }, [rows, filters.q, activeColumns, activeColFilters, sortKey, sortDir]);

  const GROUP_KEY_FIELDS = ["terminal_id","cost_type_id","containertype","container_size","container_fullness","shipment_type","corridor_from","corridor_to","transport_type","sap_vendor_code","sap_material_code","brand_company_id","payable_by","status","effective_date","expiry_date"];

  const allGroups = useMemo(() => {
    if (!groupBySlab) return null;
    const map = new Map();
    const ungrouped = [];
    for (const r of displayRows) {
      if (r.slab_unit == null || r.slab_unit === "") {
        ungrouped.push({ key: `__ungrouped__${r.id}`, rows: [r] });
        continue;
      }
      const k = GROUP_KEY_FIELDS.map(f => r[f] ?? "").join("|");
      if (!map.has(k)) map.set(k, { key: k, rows: [] });
      map.get(k).rows.push(r);
    }
    return [...map.values(), ...ungrouped];
  }, [displayRows, groupBySlab]);

  const groupedDisplay = useMemo(() => {
    if (!allGroups) return null;
    return filterMultiGroup ? allGroups.filter(g => g.rows.length > 1) : allGroups;
  }, [allGroups, filterMultiGroup]);

  const toggleGroup = (key) => setExpandedGroups(prev => {
    const next = new Set(prev);
    next.has(key) ? next.delete(key) : next.add(key);
    return next;
  });

  const pagedItems   = groupBySlab ? groupedDisplay : displayRows;
  const pageRows     = (pagedItems || []).slice((page - 1) * pageSize, page * pageSize);
  const totalPages   = Math.max(1, Math.ceil((pagedItems?.length || 0) / pageSize));

  const pageIds = groupBySlab
    ? pageRows.flatMap(grp => grp.rows.map(r => r.id))
    : pageRows.map(r => r.id);
  const allPageSelected = pageIds.length > 0 && pageIds.every(id => selectedIds.has(id));
  const somePageSelected = pageIds.some(id => selectedIds.has(id));

  function toggleAll() {
    setSelectAllPages(false);
    setSelectedIds(prev => {
      const next = new Set(prev);
      if (allPageSelected) { pageIds.forEach(id => next.delete(id)); }
      else                 { pageIds.forEach(id => next.add(id)); }
      return next;
    });
  }
  function toggleOne(id) {
    setSelectAllPages(false);
    setSelectedIds(prev => {
      const next = new Set(prev);
      next.has(id) ? next.delete(id) : next.add(id);
      return next;
    });
  }
  function clearSelection() {
    setSelectedIds(new Set());
    setSelectAllPages(false);
  }

  const handleExport = () => {
    if (!rows.length) return;
    const colDefs = [
      { key: "id",               header: "id" },
      { key: "terminal_code",    header: "terminal_code" },
      { key: "terminal_name",    header: "terminal_name" },
      { key: "cost_category",    header: "cost_category" },
      { key: "cost_type_name",   header: "cost_type_name" },
      { key: "sap_material_code",header: "sap_material_code" },
      { key: "material_name",    header: "material_name" },
      { key: "charge_unit_code", header: "charge_unit_code" },
      { key: "containertype_code",header: "containertype_code" },
      { key: "container_size",   header: "container_size" },
      { key: "container_fullness",header: "container_fullness" },
      { key: "shipment_type_code",header: "shipment_type_code" },
      { key: "rate_local",       header: "rate_local" },
      { key: "iso_code",         header: "primary_currency" },
      { key: "effective_date",   header: "effective_date" },
      { key: "expiry_date",      header: "expiry_date" },
      { key: "slab_unit_code",   header: "slab_unit" },
      { key: "slab_type_code",   header: "slab_type" },
      { key: "slab_min",         header: "slab_min" },
      { key: "slab_max",         header: "slab_max" },
      { key: "sap_vendor_code",  header: "sap_vendor_code" },
      { key: "vendor_name",      header: "vendor_name" },
      { key: "contract_ref",     header: "contract_ref" },
      { key: "contract_owner",   header: "contract_owner" },
      { key: "transport_type",   header: "transport_type" },
      { key: "route",            header: "route" },
      { key: "route_rate",       header: "route_rate" },
      { key: "corridor_from",    header: "corridor_from" },
      { key: "corridor_to",      header: "corridor_to" },
      { key: "payment_terms",      header: "payment_terms" },
      { key: "brand_company_code", header: "brand_company_code" },
      { key: "payable_by",         header: "payable_by" },
      { key: "notes",              header: "notes" },
      { key: "is_invoicerate",   header: "is_invoicerate" },
      { key: "stcy_flag",        header: "stcy_flag" },
      { key: "calculator",       header: "calculator" },
      { key: "source",           header: "source" },
      { key: "status",           header: "status" },
      { key: "created_by",       header: "created_by" },
      { key: "created_at",       header: "created_at" },
      { key: "modified_by",      header: "modified_by" },
      { key: "modified_at",      header: "modified_at" },
    ];
    const escape = v => {
      if (v == null) return "";
      const s = String(v);
      return s.includes(",") || s.includes('"') || s.includes("\n")
        ? `"${s.replace(/"/g, '""')}"` : s;
    };
    const csv = [
      colDefs.map(c => c.header).join(","),
      ...rows.map(r => colDefs.map(c => escape(r[c.key])).join(",")),
    ].join("\r\n");
    const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `ocms_rates_${new Date().toISOString().slice(0,10)}.csv`;
    a.click();
    URL.revokeObjectURL(url);
  };

  const effectiveSelectedIds = selectAllPages ? displayRows.map(r => r.id) : [...selectedIds];
  const effectiveSelectedCount = selectAllPages ? displayRows.length : selectedIds.size;

  const activeCount = rows.filter(r => r.status === "Active").length;
  const terminalCount = new Set(rows.map(r => r.terminal_id)).size;
  const expiringSoon = rows.filter(r => {
    if (!r.expiry_date) return false;
    const days = (new Date(r.expiry_date) - new Date()) / 86400000;
    return days >= 0 && days <= 30;
  }).length;

  return (
    <div>
      <PageHeader
        title="Rate Repository"
        sub={`${rows.length.toLocaleString()} rates · scoped to ${user.regions.join(", ")}`}
        right={
          <>
            {perms.aiAssistant && (
              <button className="btn" onClick={onOpenAI}>
                <Icon name="sparkle" size={14} stroke="var(--teal)" />
                Ask AI
              </button>
            )}
            {perms.export && (
              <button className="btn" onClick={handleExport} disabled={!rows.length}>
                <Icon name="download" size={14} />
                Export
              </button>
            )}
            {perms.importRates && (
              <button className="btn" onClick={onImport}>
                <Icon name="upload" size={14} />
                Import
              </button>
            )}
            {perms.editRates && effectiveSelectedCount > 1 && (
              <button className="btn" onClick={handleBulkDuplicate}>
                <Icon name="copy" size={14} />
                Duplicate ({effectiveSelectedCount})
              </button>
            )}
            {perms.editRates && effectiveSelectedCount > 0 && (
              <button className="btn" onClick={() => setShowBatchEdit(true)}>
                <Icon name="edit" size={14} />
                Edit Selected ({effectiveSelectedCount})
              </button>
            )}
            {perms.editRates && (
              <button className="btn primary" onClick={onAdd}>
                <Icon name="plus" size={14} />
                Add Rate
              </button>
            )}
          </>
        }
      />

      <div className="grid-4" style={{ marginBottom: 16 }}>
        <KPI label="Active Rates" value={loading ? "…" : activeCount.toLocaleString()} sub={`across ${terminalCount} terminals`} />
        <KPI label="Total Loaded" value={loading ? "…" : rows.length.toLocaleString()} sub="matching current filters" />
        <KPI label="Expiring < 30d" value={loading ? "…" : expiringSoon} deltaDir={expiringSoon > 0 ? "up" : undefined} />
        <KPI label="Terminals" value={loading ? "…" : terminalCount} sub="in result set" />
      </div>

      {/* View selector bar */}
      <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }}>
        <span style={{ fontSize: 12, color: "var(--ink-mute)", fontWeight: 600 }}>View:</span>
        <div style={{ position: "relative" }} ref={viewMenuRef}>
          <button
            className="btn ghost"
            style={{ fontSize: 12, padding: "3px 10px", display: "flex", alignItems: "center", gap: 6 }}
            onClick={() => setViewMenuOpen(o => !o)}
          >
            <Icon name="layout" size={13} />
            {activeView?.name || "Default"}
            <Icon name="chevron-down" size={11} />
          </button>
          {viewMenuOpen && (
            <div style={{ position: "absolute", top: 32, left: 0, background: "#fff", border: "1px solid var(--line)", borderRadius: 6, boxShadow: "var(--shadow-pop)", minWidth: 200, zIndex: 30 }}>
              {savedViews.map(v => (
                <div key={v.id}
                  onClick={() => { setActiveView(v); if (v.filters) setFilters({ ...filters, ...v.filters }); setViewMenuOpen(false); }}
                  style={{ padding: "8px 14px", fontSize: 13, cursor: "pointer", display: "flex", alignItems: "center", gap: 8,
                    background: activeView?.id === v.id ? "var(--paper)" : "" }}
                  onMouseEnter={e => e.currentTarget.style.background = "var(--paper)"}
                  onMouseLeave={e => e.currentTarget.style.background = activeView?.id === v.id ? "var(--paper)" : ""}>
                  {v.is_default && <span style={{ fontSize: 9, fontWeight: 700, color: "var(--teal)", textTransform: "uppercase" }}>DEF</span>}
                  <span style={{ flex: 1 }}>{v.name}</span>
                  {activeView?.id === v.id && <Icon name="check" size={12} />}
                </div>
              ))}
              <div style={{ borderTop: "1px solid var(--line)", padding: "6px 8px" }}>
                <button className="btn ghost" style={{ width: "100%", fontSize: 12 }}
                  onClick={() => { setViewMenuOpen(false); openManageViews(); }}>
                  Manage views…
                </button>
              </div>
            </div>
          )}
        </div>
        <button className="btn ghost" style={{ fontSize: 12, padding: "3px 10px" }}
          onClick={openManageViews}>
          <Icon name="settings" size={13} /> Columns
        </button>
        <button
          className={`btn ghost${groupBySlab ? " active" : ""}`}
          style={{ fontSize: 12, padding: "3px 10px", background: groupBySlab ? "var(--teal-light,#e0f2f4)" : "", color: groupBySlab ? "var(--teal)" : "", border: groupBySlab ? "1px solid var(--teal)" : "" }}
          onClick={() => { setGroupBySlab(v => { if (v) { setFilterMultiGroup(false); setExpandedGroups(new Set()); } return !v; }); setPage(1); }}
        >
          <Icon name="folder" size={13} /> Group by Slab
        </button>
      </div>

      <div className="filterbar">
        <FilterChipMenu keyLabel="Terminal"
          values={filters.terminals}
          options={terminals.map(t => ({ value: String(t.id), label: `${t.code} · ${t.name}` }))}
          onChange={(v) => setFilters({ ...filters, terminals: v })}
        />
        <FilterChipMenu keyLabel="Cost Type"
          values={filters.costTypes}
          options={costTypes.map(c => ({ value: String(c.id), label: c.name }))}
          onChange={(v) => setFilters({ ...filters, costTypes: v })}
        />
        <FilterChipMenu keyLabel="Status"
          values={filters.statuses}
          options={
            statusOptions.length > 0
              ? statusOptions.map(s => ({ value: s.name, label: s.name }))
              : RATE_STATUSES.map(s => ({ value: s, label: s }))
          }
          onChange={(v) => setFilters({ ...filters, statuses: v })}
        />
        <FilterChipMenu keyLabel="Ctr Type"
          values={filters.containerTypes}
          options={equipTypes.map(e => ({ value: String(e.id), label: e.name }))}
          onChange={(v) => setFilters({ ...filters, containerTypes: v })}
        />
        <FilterChipMenu keyLabel="Size"
          values={filters.containerSizes}
          options={["20", "40", "45", "All"].map(s => ({ value: s, label: s }))}
          onChange={(v) => setFilters({ ...filters, containerSizes: v })}
        />
        <FilterChipMenu keyLabel="Fullness"
          values={filters.containerFullnesses}
          options={["Empty", "Full", "All"].map(s => ({ value: s, label: s }))}
          onChange={(v) => setFilters({ ...filters, containerFullnesses: v })}
        />
        <div style={{ width: 1, height: 20, background: "var(--line)", margin: "0 4px", alignSelf: "center" }} />
        <BoolChip label="Invoice Rate" active={filters.invoiceRate}
          onChange={(v) => setFilters({ ...filters, invoiceRate: v })} />
        <BoolChip label="STCY" active={filters.stcyFlag}
          onChange={(v) => setFilters({ ...filters, stcyFlag: v })} />
        <BoolChip label="Calculator" active={filters.calculator}
          onChange={(v) => setFilters({ ...filters, calculator: v })} />
        {groupBySlab && <>
          <div style={{ width: 1, height: 20, background: "var(--line)", margin: "0 4px", alignSelf: "center" }} />
          <label style={{ display: "inline-flex", alignItems: "center", gap: 6, cursor: "pointer", userSelect: "none", fontSize: 12, color: filterMultiGroup ? "var(--teal,#0d7e96)" : "var(--ink-mute)", fontWeight: filterMultiGroup ? 600 : 400, whiteSpace: "nowrap" }}>
            <input type="checkbox" checked={filterMultiGroup} onChange={e => { setFilterMultiGroup(e.target.checked); setPage(1); }} style={{ accentColor: "var(--teal,#0d7e96)", width: 13, height: 13 }} />
            Multi-slab only
          </label>
        </>}
        <div style={{ marginLeft: "auto" }} />
        <div className="search">
          <Icon name="search" size={14} />
          <input
            placeholder={`Search ${activeColumns.length} fields…`}
            value={filters.q}
            onChange={e => { setFilters({ ...filters, q: e.target.value }); setPage(1); }}
          />
        </div>
        <button className="btn ghost" onClick={load}><Icon name="refresh" size={14} /></button>
      </div>

      <div className="card">
        <div className="table-wrap">
          <table className="table">
            <thead>
              <tr>
                <th style={{ width: groupBySlab ? 52 : 28 }}>
                  <div style={{ display: "flex", alignItems: "center", gap: 4 }}>
                    <input type="checkbox"
                      checked={allPageSelected}
                      ref={el => { if (el) el.indeterminate = somePageSelected && !allPageSelected; }}
                      onChange={toggleAll}
                    />
                    {groupBySlab && (() => {
                      const multiGroups = (groupedDisplay || []).filter(g => g.rows.length > 1);
                      if (!multiGroups.length) return null;
                      const allExpanded = multiGroups.every(g => expandedGroups.has(g.key));
                      return (
                        <button
                          title={allExpanded ? "Collapse all groups" : "Expand all groups"}
                          onClick={() => {
                            if (allExpanded) {
                              setExpandedGroups(new Set());
                            } else {
                              setExpandedGroups(new Set(multiGroups.map(g => g.key)));
                            }
                          }}
                          style={{ background: "none", border: "none", padding: 0, cursor: "pointer", color: "var(--teal,#0d7e96)", display: "flex", alignItems: "center", lineHeight: 1 }}
                        >
                          <Icon name={allExpanded ? "folder-open" : "folder"} size={15} stroke="var(--teal,#0d7e96)" strokeWidth={1.8} />
                        </button>
                      );
                    })()}
                  </div>
                </th>
                {activeColumns.map(key => {
                  const colDef = ALL_COLUMNS.find(c => c.key === key);
                  const isNum = NUMERIC_KEYS.has(key);
                  const active = sortKey === key;
                  return (
                    <th key={key}
                      className={isNum ? "num" : ""}
                      style={{ cursor: "pointer", userSelect: "none", whiteSpace: "nowrap" }}
                      onClick={() => handleSort(key)}
                    >
                      {colDef?.label || key}
                      <span style={{ marginLeft: 4, opacity: active ? 1 : 0.25, fontSize: 11 }}>
                        {active && sortDir === "desc" ? "▼" : "▲"}
                      </span>
                    </th>
                  );
                })}
                <th style={{ width: 28 }}></th>
              </tr>
              <tr style={{ background: "var(--surface-2, #f5f5f5)" }}>
                <th style={{ padding: "2px 4px" }}></th>
                {activeColumns.map(key => {
                  const selected = colFilters[key] || [];
                  const isActive = selected.length > 0;
                  const uniqueVals = (() => {
                    const all = [...new Set(rows.map(r => { const v = r[key]; return v == null ? "" : String(v); }))];
                    const nonEmpty = all.filter(v => v !== "").sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
                    return all.includes("") ? [...nonEmpty, ""] : nonEmpty;
                  })();
                  return (
                    <th key={key} style={{ padding: "2px 3px", position: "relative" }}>
                      <ColDropFilter
                        colKey={key}
                        options={uniqueVals}
                        selected={selected}
                        onChange={vals => { setColFilters(prev => ({ ...prev, [key]: vals })); setPage(1); }}
                      />
                    </th>
                  );
                })}
                <th style={{ padding: "2px 4px" }}>
                  {Object.keys(activeColFilters).length > 0 && (
                    <button
                      title="Clear all column filters"
                      onClick={() => { setColFilters({}); setPage(1); }}
                      style={{ background: "none", border: "none", cursor: "pointer", color: "var(--ink-mute)", fontSize: 13, lineHeight: 1, padding: 2 }}
                    >✕</button>
                  )}
                </th>
              </tr>
            </thead>
            <tbody>
              {loading ? (
                <tr><td colSpan={activeColumns.length + 2} style={{ textAlign: "center", padding: 32, color: "var(--ink-mute)" }}>Loading rates…</td></tr>
              ) : pageRows.length === 0 ? (
                <tr><td colSpan={activeColumns.length + 2} style={{ textAlign: "center", padding: 32, color: "var(--ink-mute)" }}>No rates found. Try adjusting filters or import a rate sheet.</td></tr>
              ) : groupBySlab ? pageRows.map(grp => {
                const rep = grp.rows[0];
                const multi = grp.rows.length > 1;
                const expanded = expandedGroups.has(grp.key);
                return (
                  <React.Fragment key={grp.key}>
                    {/* ── Group summary row ── */}
                    <tr onClick={() => multi ? toggleGroup(grp.key) : onOpenRate(rep)} style={{ cursor: "pointer", background: multi ? (expanded ? "#e0f2f4" : "#f0fafa") : "" }}>
                      <td onClick={e => e.stopPropagation()} style={{ verticalAlign: "middle", padding: "0 4px" }}>
                        <div style={{ display: "flex", alignItems: "center", gap: 5 }}>
                          <input type="checkbox" checked={selectAllPages || grp.rows.every(r => selectedIds.has(r.id))} onChange={() => grp.rows.forEach(r => toggleOne(r.id))} />
                          {multi && (
                            <button
                              onClick={e => { e.stopPropagation(); toggleGroup(grp.key); }}
                              title={expanded ? "Collapse" : `Expand ${grp.rows.length} slab tiers`}
                              style={{ background: "none", border: "none", padding: 0, cursor: "pointer", color: "var(--teal,#0d7e96)", display: "flex", alignItems: "center", lineHeight: 1 }}
                            >
                              <Icon name={expanded ? "folder-open" : "folder"} size={15} stroke="var(--teal,#0d7e96)" strokeWidth={1.8} />
                            </button>
                          )}
                        </div>
                      </td>
                      {activeColumns.map(key => {
                        const colDef = ALL_COLUMNS.find(c => c.key === key);
                        const blank = multi && ["rate_local","rate_usd","is_invoicerate","stcy_flag","calculator"].includes(key);
                        return <td key={key} className={colDef?.cls || ""}>{blank ? <span style={{ color: "var(--line)", fontSize: 11 }}>—</span> : renderCell(colDef || { key }, rep, roeMap)}</td>;
                      })}
                      <td onClick={e => e.stopPropagation()}>
                        <RowActionMenu onEdit={() => onOpenRate(rep)} onDuplicate={onAddPrefill && perms.editRates ? () => handleDuplicate(rep) : null} onSupersede={perms.editRates && rep.status !== "Inactive" ? () => handleSupersede(rep) : null} onDelete={perms.editRates ? () => handleDelete(rep) : null} rateStatus={rep.status} />
                      </td>
                    </tr>
                    {/* ── Expanded slab sub-rows ── */}
                    {multi && expanded && grp.rows.map((r, i) => {
                      const currency = r.currency_code || r.iso_code || "";
                      const SlabCell = ({ label, value, mono, color, width = 72 }) => (
                        <div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: width, width }}>
                          <span style={{ fontSize: 9, color: "var(--ink-mute)", textTransform: "uppercase", letterSpacing: "0.05em" }}>{label}</span>
                          {value != null && value !== "" && value !== false
                            ? <span style={{ fontSize: 12, fontWeight: 600, color: color || "var(--ink)", fontFamily: mono ? "ui-monospace,monospace" : undefined }}>{value}</span>
                            : <span style={{ fontSize: 12, color: "var(--line)" }}>—</span>}
                        </div>
                      );
                      return (
                        <tr key={r.id} onClick={() => onOpenRate(r)} style={{ cursor: "pointer", background: i % 2 === 0 ? "#f0f9fa" : "#e8f5f7", borderLeft: "3px solid var(--teal,#0d7e96)" }}>
                          <td onClick={e => e.stopPropagation()} style={{ verticalAlign: "middle", paddingLeft: 28 }}>
                            <input type="checkbox" checked={selectAllPages || selectedIds.has(r.id)} onChange={() => toggleOne(r.id)} />
                          </td>
                          <td colSpan={activeColumns.length} style={{ padding: "6px 14px" }}>
                            <div style={{ display: "flex", alignItems: "flex-start", gap: 0 }}>
                              {/* Slab range */}
                              <SlabCell label="Slab Unit"  value={r.slab_unit_code}                              width={80} />
                              <SlabCell label="Slab Type"  value={r.slab_type_code}                              width={80} />
                              <SlabCell label="Min"        value={r.slab_min != null ? fmt.num(r.slab_min) : null} mono width={72} />
                              <div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 20, width: 20, alignItems: "center" }}>
                                <span style={{ fontSize: 9, color: "transparent" }}>·</span>
                                <span style={{ fontSize: 13, color: "var(--ink-mute)", lineHeight: "18px" }}>→</span>
                              </div>
                              <SlabCell label="Max"        value={r.slab_max != null ? fmt.num(r.slab_max) : "∞"} mono width={72} />
                              {/* Divider */}
                              <div style={{ width: 1, height: 36, background: "var(--line)", alignSelf: "center", margin: "0 12px" }} />
                              {/* Rate & flags */}
                              <SlabCell label={`Rate (${currency || "local"})`} value={r.rate_local != null ? fmt.num(r.rate_local) : null} mono color="var(--teal,#0d7e96)" width={96} />
                              <SlabCell label="Rate (USD)" value={r.rate_local != null ? fmt.usd(r.rate_local / (roeMap[r.iso_code] || 1)) : null} mono width={88} />
                              {/* Divider */}
                              <div style={{ width: 1, height: 36, background: "var(--line)", alignSelf: "center", margin: "0 12px" }} />
                              {/* Flag badges — fixed width so they always take up the same space */}
                              <div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 80, width: 80 }}>
                                <span style={{ fontSize: 9, color: "var(--ink-mute)", textTransform: "uppercase", letterSpacing: "0.05em" }}>Invoice Rate</span>
                                {r.is_invoicerate ? <span style={{ fontSize: 11, fontWeight: 600, color: "#0369a1", background: "#e0f2fe", borderRadius: 3, padding: "1px 6px", display: "inline-block", width: "fit-content" }}>Yes</span> : <span style={{ fontSize: 12, color: "var(--line)" }}>—</span>}
                              </div>
                              <div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 56, width: 56 }}>
                                <span style={{ fontSize: 9, color: "var(--ink-mute)", textTransform: "uppercase", letterSpacing: "0.05em" }}>STCY</span>
                                {r.stcy_flag ? <span style={{ fontSize: 11, fontWeight: 600, color: "#7c3aed", background: "#ede9fe", borderRadius: 3, padding: "1px 6px", display: "inline-block", width: "fit-content" }}>Yes</span> : <span style={{ fontSize: 12, color: "var(--line)" }}>—</span>}
                              </div>
                              <div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 72, width: 72 }}>
                                <span style={{ fontSize: 9, color: "var(--ink-mute)", textTransform: "uppercase", letterSpacing: "0.05em" }}>Calculator</span>
                                {r.calculator ? <span style={{ fontSize: 11, fontWeight: 600, color: "#065f46", background: "#d1fae5", borderRadius: 3, padding: "1px 6px", display: "inline-block", width: "fit-content" }}>Yes</span> : <span style={{ fontSize: 12, color: "var(--line)" }}>—</span>}
                              </div>
                            </div>
                          </td>
                          <td onClick={e => e.stopPropagation()}>
                            <RowActionMenu onEdit={() => onOpenRate(r)} onDuplicate={onAddPrefill && perms.editRates ? () => handleDuplicate(r) : null} onSupersede={perms.editRates && r.status !== "Inactive" ? () => handleSupersede(r) : null} onDelete={perms.editRates ? () => handleDelete(r) : null} rateStatus={r.status} />
                          </td>
                        </tr>
                      );
                    })}
                  </React.Fragment>
                );
              }) : pageRows.map(r => (
                <tr key={r.id} onClick={() => onOpenRate(r)} style={{ cursor: "pointer" }}>
                  <td onClick={(e) => e.stopPropagation()}>
                    <input type="checkbox" checked={selectAllPages || selectedIds.has(r.id)} onChange={() => toggleOne(r.id)} />
                  </td>
                  {activeColumns.map(key => {
                    const colDef = ALL_COLUMNS.find(c => c.key === key);
                    return <td key={key} className={colDef?.cls || ""}>{renderCell(colDef || { key }, r, roeMap)}</td>;
                  })}
                  <td onClick={e => e.stopPropagation()}>
                    <RowActionMenu
                      onEdit={() => onOpenRate(r)}
                      onDuplicate={onAddPrefill && perms.editRates ? () => handleDuplicate(r) : null}
                      onSupersede={perms.editRates && r.status !== "Inactive" ? () => handleSupersede(r) : null}
                      onDelete={perms.editRates ? () => handleDelete(r) : null}
                      rateStatus={r.status}
                    />
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
        {effectiveSelectedCount > 0 && (
          <div style={{ padding: "8px 16px", background: "#eff6ff", borderTop: "1px solid #bfdbfe", display: "flex", alignItems: "center", gap: 12, fontSize: 13 }}>
            <span style={{ color: "#1d4ed8" }}>
              {selectAllPages
                ? `All ${displayRows.length} records are selected.`
                : `${selectedIds.size} record${selectedIds.size !== 1 ? "s" : ""} on this page selected.`}
            </span>
            {!selectAllPages && allPageSelected && displayRows.length > pageIds.length && (
              <button
                style={{ background: "none", border: "none", color: "#1d4ed8", cursor: "pointer", textDecoration: "underline", padding: 0, fontSize: 13 }}
                onClick={() => setSelectAllPages(true)}
              >
                Select all {displayRows.length} records
              </button>
            )}
            <button
              style={{ background: "none", border: "none", color: "#6b7280", cursor: "pointer", textDecoration: "underline", padding: 0, fontSize: 13, marginLeft: "auto" }}
              onClick={clearSelection}
            >
              Clear selection
            </button>
          </div>
        )}
        <div className="card-head" style={{ borderTop: "1px solid var(--line)", borderBottom: 0, background: "#fafbfc" }}>
          <div className="muted tiny">
            Showing <b>{(page - 1) * pageSize + 1}</b>–<b>{Math.min(page * pageSize, pagedItems?.length || 0)}</b> of <b>{(pagedItems?.length || 0).toLocaleString()}</b>
          </div>
          <div className="row gap-4" style={{ marginLeft: "auto" }}>
            <button className="btn ghost" disabled={page === 1} onClick={() => setPage(p => Math.max(1, p - 1))}>Previous</button>
            <span className="mono tiny muted" style={{ padding: "0 8px" }}>Page {page} of {totalPages}</span>
            <button className="btn ghost" disabled={page >= totalPages} onClick={() => setPage(p => p + 1)}>Next</button>
          </div>
        </div>
      </div>

      {showBatchEdit && (
        <BatchEditModal
          ids={effectiveSelectedIds}
          onClose={() => setShowBatchEdit(false)}
          onDone={() => { setShowBatchEdit(false); clearSelection(); load(); }}
        />
      )}

      {manageViewsInitId !== null && (
        <ManageViewsModal
          views={savedViews}
          currentFilters={filters}
          initialViewId={manageViewsInitId}
          user={user}
          perms={perms}
          onClose={() => setManageViewsInitId(null)}
          onApply={(v) => {
            setActiveView(v);
            if (v.filters) setFilters(f => ({ ...f, ...v.filters }));
          }}
          onSaved={async () => { await loadViews(); }}
        />
      )}
    </div>
  );
}

function BoolChip({ label, active, onChange }) {
  return (
    <button
      onClick={() => onChange(!active)}
      style={{
        display: "inline-flex", alignItems: "center", gap: 6,
        padding: "4px 10px", borderRadius: 20, fontSize: 12, fontWeight: 500,
        border: `1.5px solid ${active ? "var(--teal, #009eb4)" : "var(--line)"}`,
        background: active ? "var(--teal-light, #e6f6f9)" : "#fff",
        color: active ? "var(--teal, #009eb4)" : "var(--ink-mute)",
        cursor: "pointer", userSelect: "none", transition: "all 0.15s",
      }}
    >
      <span style={{
        width: 14, height: 14, borderRadius: 3, flexShrink: 0,
        border: `1.5px solid ${active ? "var(--teal, #009eb4)" : "var(--ink-mute)"}`,
        background: active ? "var(--teal, #009eb4)" : "#fff",
        display: "flex", alignItems: "center", justifyContent: "center",
      }}>
        {active && <svg width="9" height="7" viewBox="0 0 9 7" fill="none">
          <path d="M1 3.5L3.5 6L8 1" stroke="#fff" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
        </svg>}
      </span>
      {label}
    </button>
  );
}

const BATCH_FIELDS = [
  // ── Terminal / Corridor ──────────────────────────────────────────
  { type: "section", label: "Terminal / Corridor" },
  { key: "transport_type",     label: "Transport Type",     type: "opts", opts: ["Free","Rail","Barge","Road","Sea"] },
  { key: "route",              label: "Route",              type: "text" },
  { key: "corridor_from",      label: "Corridor From",      type: "text" },
  { key: "corridor_to",        label: "Corridor To",        type: "text" },

  // ── Contract Info ────────────────────────────────────────────────
  { type: "section", label: "Contract Info" },
  { key: "brand_company_id",   label: "Brand Company",      type: "meta",     meta: "brand-companies" },
  { key: "contract_ref",       label: "Contract Ref",       type: "text" },
  { key: "contract_owner",     label: "Contract Owner",     type: "text" },
  { key: "sap_vendor_code",    label: "SAP Vendor Code",    type: "meta-str", meta: "vendors",       valKey: "code", lblKey: "name", searchable: true },
  { key: "payment_terms",      label: "Payment Terms",      type: "meta-str", meta: "payment-terms", valKey: "name", lblKey: "name", searchable: true },

  // ── Cost Type & Container ────────────────────────────────────────
  { type: "section", label: "Cost Type & Container" },
  { key: "cost_type_id",       label: "Cost Type",          type: "meta", meta: "cost-types" },
  { key: "cost_category",      label: "Cost Category",      type: "opts", opts: ["CYB","CYM","PE"] },
  { key: "sap_material_code",  label: "SAP Material Code",  type: "meta-str", meta: "materials",     valKey: "code", lblKey: "name", searchable: true },
  { key: "charge_unit_id",     label: "Charge Unit",        type: "meta", meta: "charge-units" },
  { key: "containertype",      label: "Container Type",     type: "meta", meta: "equipment-types" },
  { key: "container_size",     label: "Container Size",     type: "opts", opts: ["20","40","45","All"] },
  { key: "container_fullness", label: "Container Fullness", type: "opts", opts: ["Empty","Full","All"] },
  { key: "shipment_type",      label: "Shipment Type",      type: "meta", meta: "move-types" },

  { key: "payable_by",          label: "Payable By",         type: "opts", opts: ["Maersk","Customer"] },

  // ── Rate ─────────────────────────────────────────────────────────
  { type: "section", label: "Rate" },
  { key: "primary_currency",   label: "Currency",           type: "meta", meta: "currencies", lblKey: "iso_code" },
  { key: "effective_date",     label: "Effective Date",     type: "date" },
  { key: "expiry_date",        label: "Expiry Date",        type: "date" },
  { key: "status",             label: "Status",             type: "status" },
  { key: "is_invoicerate",     label: "Invoice Rate",       type: "bool" },
  { key: "stcy_flag",          label: "STCY Flag",          type: "bool" },
  { key: "calculator",         label: "Calculator",         type: "bool" },

  // ── Slab / Tier ──────────────────────────────────────────────────
  { type: "section", label: "Slab / Tier" },
  { key: "slab_unit",          label: "Slab Unit",          type: "meta", meta: "slab-units" },
  { key: "slab_type",          label: "Slab Type",          type: "meta", meta: "slab-types" },
  { key: "slab_min",           label: "Slab Min",           type: "number" },
  { key: "slab_max",           label: "Slab Max",           type: "number" },

  // ── Others ───────────────────────────────────────────────────────
  { type: "section", label: "Others" },
  { key: "notes",              label: "Notes",              type: "textarea" },
];

function BatchEditModal({ ids, onClose, onDone }) {
  const [enabled, setEnabled] = useState({});
  const [values, setValues] = useState({});
  const [meta, setMeta] = useState({});
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const endpoints = ["cost-types","charge-units","equipment-types","move-types","currencies","slab-units","slab-types","rate-statuses","vendors","payment-terms","materials","brand-companies"];
    Promise.all(endpoints.map(e => apiFetchMeta(e).catch(() => []))).then(results => {
      const m = {};
      endpoints.forEach((e, i) => { m[e] = Array.isArray(results[i]) ? results[i] : []; });
      setMeta(m);
    });
  }, []);

  const toggle = (key) => {
    setEnabled(prev => ({ ...prev, [key]: !prev[key] }));
    if (!enabled[key] && values[key] === undefined) {
      setValues(prev => ({ ...prev, [key]: "" }));
    }
  };

  const setVal = (key, val) => setValues(prev => ({ ...prev, [key]: val }));

  const renderControl = (f) => {
    const dis = !enabled[f.key];
    const val = values[f.key] ?? "";
    const cls = "select" + (dis ? " disabled" : "");
    const style = { width: "100%", opacity: dis ? 0.45 : 1 };
    if (f.type === "status") {
      return (
        <select className={cls} style={style} disabled={dis} value={val} onChange={e => setVal(f.key, e.target.value)}>
          <option value="">— select —</option>
          {(meta["rate-statuses"] || []).filter(s => s.is_active).map(s => <option key={s.id} value={s.name}>{s.name}</option>)}
        </select>
      );
    }
    if (f.type === "meta") {
      const opts = meta[f.meta] || [];
      const lblKey = f.lblKey || "name";
      return (
        <select className={cls} style={style} disabled={dis} value={val} onChange={e => setVal(f.key, e.target.value === "" ? null : parseInt(e.target.value))}>
          <option value="">— select —</option>
          {opts.filter(o => o.is_active !== false).map(o => <option key={o.id} value={o.id}>{o[lblKey]}{o.name && lblKey !== "name" ? ` – ${o.name}` : ""}</option>)}
        </select>
      );
    }
    if (f.type === "opts") {
      return (
        <select className={cls} style={style} disabled={dis} value={val} onChange={e => setVal(f.key, e.target.value)}>
          <option value="">— select —</option>
          {f.opts.map(o => <option key={o} value={o}>{o}</option>)}
        </select>
      );
    }
    if (f.type === "meta-str") {
      const opts = (meta[f.meta] || []).filter(o => o.is_active !== false);
      const vKey = f.valKey || "name";
      const lKey = f.lblKey || "name";
      if (f.searchable) {
        return (
          <div style={{ opacity: dis ? 0.45 : 1, pointerEvents: dis ? "none" : "auto" }}>
            <SearchSelect
              options={opts}
              value={val}
              onChange={v => setVal(f.key, v || null)}
              placeholder="Search…"
              getLabel={o => lKey !== vKey ? `${o[lKey]} (${o[vKey]})` : o[lKey]}
              getValue={o => o[vKey]}
            />
          </div>
        );
      }
      return (
        <select className={cls} style={style} disabled={dis} value={val} onChange={e => setVal(f.key, e.target.value || null)}>
          <option value="">— select —</option>
          {opts.map(o => <option key={o[vKey]} value={o[vKey]}>{o[lKey]}{lKey !== vKey ? ` (${o[vKey]})` : ""}</option>)}
        </select>
      );
    }
    if (f.type === "date") return <input type="date" className="input" style={style} disabled={dis} value={val} onChange={e => setVal(f.key, e.target.value)} />;
    if (f.type === "number") return <input type="number" className="input" style={style} disabled={dis} value={val} onChange={e => setVal(f.key, e.target.value)} />;
    if (f.type === "textarea") return <textarea className="input" rows={2} style={style} disabled={dis} value={val} onChange={e => setVal(f.key, e.target.value)} />;
    if (f.type === "bool") return (
      <select className={cls} style={style} disabled={dis} value={val === "" ? "" : String(val)} onChange={e => setVal(f.key, e.target.value === "" ? "" : e.target.value === "true")}>
        <option value="">— select —</option>
        <option value="true">Yes</option>
        <option value="false">No</option>
      </select>
    );
    return <input type="text" className="input" style={style} disabled={dis} value={val} onChange={e => setVal(f.key, e.target.value)} />;
  };

  const handleSubmit = async () => {
    const updates = {};
    for (const f of BATCH_FIELDS) {
      if (enabled[f.key] && values[f.key] !== undefined && values[f.key] !== "") {
        updates[f.key] = values[f.key];
      }
    }
    if (Object.keys(updates).length === 0) { setError("Please enable and set at least one field."); return; }
    setSaving(true); setError(null);
    try {
      const res = await apiFetch("/api/rates/batch", { method: "PATCH", body: JSON.stringify({ ids, updates }) });
      onDone(res.updated);
    } catch (e) {
      setError(e.message || "Batch update failed");
    } finally {
      setSaving(false);
    }
  };

  return (
    <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.4)", zIndex: 2000, display: "flex", alignItems: "center", justifyContent: "center" }}>
      <div style={{ background: "#fff", borderRadius: 10, width: 640, maxHeight: "88vh", display: "flex", flexDirection: "column", boxShadow: "0 8px 40px rgba(0,0,0,0.18)" }}>
        <div style={{ padding: "16px 20px", borderBottom: "1px solid var(--line)", display: "flex", alignItems: "center", justifyContent: "space-between" }}>
          <div>
            <div style={{ fontWeight: 600, fontSize: 15 }}>Batch Edit</div>
            <div style={{ color: "var(--ink-mute)", fontSize: 12, marginTop: 2 }}>{ids.length} rate{ids.length !== 1 ? "s" : ""} selected — check a field to enable editing</div>
          </div>
          <button className="icon-btn" onClick={onClose}>×</button>
        </div>
        <div style={{ overflowY: "auto", flex: 1, padding: "8px 20px" }}>
          {BATCH_FIELDS.map((f, i) => {
            if (f.type === "section") {
              return (
                <div key={`sec-${i}`} style={{ fontSize: 11, fontWeight: 700, color: "var(--teal,#0d7e96)", textTransform: "uppercase", letterSpacing: "0.07em", padding: i === 0 ? "10px 0 4px" : "16px 0 4px", borderBottom: "2px solid var(--teal,#0d7e96)" }}>
                  {f.label}
                </div>
              );
            }
            return (
              <div key={f.key} style={{ display: "grid", gridTemplateColumns: "20px 180px 1fr", alignItems: "center", gap: 10, padding: "7px 0", borderBottom: "1px solid var(--line-light, #f0f0f0)" }}>
                <input type="checkbox" checked={!!enabled[f.key]} onChange={() => toggle(f.key)} />
                <label style={{ fontSize: 13, color: enabled[f.key] ? "var(--ink)" : "var(--ink-mute)", fontWeight: enabled[f.key] ? 500 : 400 }}>{f.label}</label>
                <div>{renderControl(f)}</div>
              </div>
            );
          })}
        </div>
        {error && <div style={{ padding: "8px 20px", color: "var(--red)", fontSize: 13 }}>{error}</div>}
        <div style={{ padding: "12px 20px", borderTop: "1px solid var(--line)", display: "flex", justifyContent: "flex-end", gap: 8 }}>
          <button className="btn" onClick={onClose}>Cancel</button>
          <button className="btn primary" onClick={handleSubmit} disabled={saving}>
            {saving ? "Updating…" : `Confirm Batch Update (${ids.length})`}
          </button>
        </div>
      </div>
    </div>
  );
}

function RowActionMenu({ onEdit, onDuplicate, onSupersede, onDelete, rateStatus }) {
  const [open, setOpen] = useState(false);
  const ref = useRef();

  useEffect(() => {
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", h);
    return () => document.removeEventListener("mousedown", h);
  }, []);

  const item = (label, onClick, danger) => (
    <div
      onClick={() => { setOpen(false); onClick(); }}
      style={{
        padding: "7px 14px", cursor: "pointer", fontSize: 13, whiteSpace: "nowrap",
        color: danger ? "var(--red)" : "var(--ink)",
        borderRadius: 4,
      }}
      onMouseEnter={e => e.currentTarget.style.background = "var(--paper)"}
      onMouseLeave={e => e.currentTarget.style.background = ""}
    >
      {label}
    </div>
  );

  const disabledItem = (label, tooltip) => (
    <div
      title={tooltip}
      style={{
        padding: "7px 14px", cursor: "not-allowed", fontSize: 13, whiteSpace: "nowrap",
        color: "var(--muted)", borderRadius: 4, opacity: 0.5,
      }}
    >
      {label}
    </div>
  );

  const isDraft = rateStatus === "Draft";

  return (
    <div ref={ref} style={{ position: "relative", display: "inline-block" }}>
      <button
        className="icon-btn"
        style={{ width: 24, height: 24 }}
        onClick={e => { e.stopPropagation(); setOpen(o => !o); }}
      >
        <Icon name="dots" size={14} />
      </button>
      {open && (
        <div
          style={{
            position: "fixed", background: "#fff", border: "1px solid var(--line)",
            borderRadius: 6, boxShadow: "var(--shadow-pop)", zIndex: 9999, padding: "4px 0",
          }}
          ref={el => {
            if (!el) return;
            const btn = ref.current?.querySelector("button");
            if (!btn) return;
            const rect = btn.getBoundingClientRect();
            el.style.top = (rect.bottom + 2) + "px";
            el.style.left = Math.max(4, rect.right - el.offsetWidth) + "px";
          }}
          onClick={e => e.stopPropagation()}
        >
          {item("View / Edit", onEdit)}
          {onDuplicate && item("Duplicate", onDuplicate)}
          {onSupersede && item("Deactivate", onSupersede, true)}
          {onDelete && (
            isDraft
              ? item("Delete", onDelete, true)
              : disabledItem("Delete", "Only Draft rates can be deleted. Move to Draft first.")
          )}
        </div>
      )}
    </div>
  );
}

function ColDropFilter({ colKey, options = [], selected = [], onChange }) {
  const [open, setOpen] = useState(false);
  const [search, setSearch] = useState("");
  const ref = useRef();

  useEffect(() => {
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) { setOpen(false); setSearch(""); } };
    document.addEventListener("mousedown", h);
    return () => document.removeEventListener("mousedown", h);
  }, []);

  const sel = new Set(selected);
  const filtered = search.trim()
    ? options.filter(v => v.toLowerCase().includes(search.toLowerCase()))
    : options;

  const toggle = (v) => {
    const next = new Set(sel);
    next.has(v) ? next.delete(v) : next.add(v);
    onChange([...next]);
  };

  const allSel = filtered.length > 0 && filtered.every(v => sel.has(v));

  const btnLabel = sel.size === 0 ? "▾" : sel.size === 1 ? `${[...sel][0]} ▾` : `${sel.size} ▾`;
  const isActive = sel.size > 0;

  return (
    <div ref={ref} style={{ position: "relative" }}>
      <button
        onClick={e => { e.stopPropagation(); setOpen(o => !o); }}
        style={{
          width: "100%", fontSize: 11, padding: "2px 4px", border: "1px solid var(--line)",
          borderRadius: 3, background: isActive ? "var(--teal-50, #f0fdfa)" : "white",
          color: isActive ? "var(--teal)" : "var(--ink-mute)", cursor: "pointer",
          textAlign: "left", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
          maxWidth: 140,
        }}
      >
        {btnLabel}
      </button>
      {open && (
        <div
          onClick={e => e.stopPropagation()}
          style={{
            position: "fixed", background: "#fff", border: "1px solid var(--line)",
            borderRadius: 6, boxShadow: "var(--shadow-pop)", minWidth: 200, maxWidth: 280,
            zIndex: 9999,
          }}
          ref={el => {
            if (!el) return;
            const btn = ref.current?.querySelector("button");
            if (!btn) return;
            const rect = btn.getBoundingClientRect();
            el.style.top = (rect.bottom + 2) + "px";
            el.style.left = rect.left + "px";
          }}
        >
          <div style={{ padding: "6px 8px 4px" }}>
            <input
              autoFocus
              placeholder="Search…"
              value={search}
              onChange={e => setSearch(e.target.value)}
              style={{ width: "100%", boxSizing: "border-box", fontSize: 12, padding: "3px 6px", border: "1px solid var(--line)", borderRadius: 3, outline: "none" }}
            />
          </div>
          <div style={{ maxHeight: 240, overflowY: "auto", padding: "2px 4px" }}>
            {filtered.length === 0 && <div style={{ padding: "8px 10px", fontSize: 12, color: "var(--ink-mute)" }}>No options</div>}
            {filtered.map(v => (
              <div key={v} onClick={() => toggle(v)}
                style={{ padding: "5px 8px", borderRadius: 4, cursor: "pointer", display: "flex", alignItems: "center", gap: 7, userSelect: "none", fontSize: 12 }}
                onMouseEnter={e => e.currentTarget.style.background = "var(--paper)"}
                onMouseLeave={e => e.currentTarget.style.background = ""}
              >
                <input type="checkbox" readOnly checked={sel.has(v)}
                  style={{ width: 13, height: 13, accentColor: "var(--teal)", pointerEvents: "none", flexShrink: 0 }} />
                <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{v === "" ? "(blank)" : v}</span>
              </div>
            ))}
          </div>
          <div style={{ borderTop: "1px solid var(--line)", padding: "5px 8px", display: "flex", gap: 6 }}>
            <button className="btn ghost" style={{ fontSize: 11, padding: "2px 7px" }}
              onClick={() => { const next = new Set(sel); filtered.forEach(v => next.add(v)); onChange([...next]); }}
              disabled={allSel}>
              {search.trim() ? "Select results" : "Select all"}
            </button>
            <button className="btn ghost" style={{ fontSize: 11, padding: "2px 7px" }}
              onClick={() => onChange([])} disabled={sel.size === 0}>
              Clear
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

function FilterChipMenu({ keyLabel, values = [], options = [], onChange }) {
  const [open, setOpen] = useState(false);
  const [search, setSearch] = useState("");
  const ref = useRef();

  useEffect(() => {
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) { setOpen(false); setSearch(""); } };
    document.addEventListener("mousedown", h);
    return () => document.removeEventListener("mousedown", h);
  }, []);

  const opts = options.map(o => typeof o === "string" ? { value: o, label: o } : o);
  const filtered = search.trim()
    ? opts.filter(o => o.label.toLowerCase().includes(search.toLowerCase()))
    : opts;
  const selected = new Set(values);

  const toggle = (v) => {
    const next = new Set(selected);
    next.has(v) ? next.delete(v) : next.add(v);
    onChange([...next]);
  };

  const clear = (e) => { e && e.stopPropagation(); onChange([]); };

  const selectVisible = (e) => {
    e && e.stopPropagation();
    const next = new Set(selected);
    filtered.forEach(o => next.add(o.value));
    onChange([...next]);
  };

  const allVisibleSelected = filtered.length > 0 && filtered.every(o => selected.has(o.value));

  let displayValue = "";
  if (selected.size === 1) {
    const v = [...selected][0];
    displayValue = opts.find(o => o.value === v)?.label || v;
  } else if (selected.size > 1) {
    displayValue = `${selected.size} selected`;
  }

  return (
    <div style={{ position: "relative" }} ref={ref}>
      <FilterChip keyLabel={keyLabel} value={displayValue} set={selected.size > 0}
        onClick={() => setOpen(o => !o)} onClear={clear} />
      {open && (
        <div style={{ position: "absolute", top: 34, left: 0, background: "#fff", border: "1px solid var(--line)", borderRadius: 6, boxShadow: "var(--shadow-pop)", minWidth: 240, zIndex: 20 }}>
          <div style={{ padding: "8px 8px 4px" }}>
            <div className="search" style={{ width: "100%" }}>
              <Icon name="search" size={13} />
              <input
                style={{ fontSize: 12, width: "100%" }}
                placeholder={`Search ${keyLabel}…`}
                value={search}
                onChange={e => setSearch(e.target.value)}
                autoFocus
              />
            </div>
          </div>
          <div style={{ maxHeight: 280, overflowY: "auto", padding: "2px 4px" }}>
            {filtered.length === 0 && (
              <div className="tiny muted" style={{ padding: "10px 12px" }}>No results</div>
            )}
            {filtered.map(o => (
              <div key={o.value} onClick={() => toggle(o.value)}
                style={{ padding: "6px 10px", borderRadius: 4, cursor: "pointer", display: "flex", alignItems: "center", gap: 8, userSelect: "none" }}
                onMouseEnter={e => e.currentTarget.style.background = "var(--paper)"}
                onMouseLeave={e => e.currentTarget.style.background = ""}
              >
                <input type="checkbox" readOnly checked={selected.has(o.value)}
                  style={{ width: 14, height: 14, accentColor: "var(--teal)", pointerEvents: "none", flexShrink: 0 }} />
                <span style={{ fontSize: 13 }}>{o.label}</span>
              </div>
            ))}
          </div>
          {filtered.length > 0 && (
            <div style={{ borderTop: "1px solid var(--line)", padding: "6px 10px", display: "flex", gap: 6 }}>
              <button className="btn ghost" style={{ fontSize: 11, padding: "2px 8px" }}
                onClick={selectVisible} disabled={allVisibleSelected}>
                {search.trim() ? "Select results" : "Select all"}
              </button>
              {selected.size > 0 && (
                <button className="btn ghost" style={{ fontSize: 11, padding: "2px 8px" }} onClick={clear}>
                  Clear all
                </button>
              )}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

function RateHistoryTab({ rateId }) {
  const [history, setHistory] = useState(null);
  const [loading, setLoading] = useState(true);
  const [err, setErr] = useState(null);

  useEffect(() => {
    setLoading(true); setErr(null);
    apiFetch(`/api/rates/${rateId}/history`)
      .then(d => { setHistory(Array.isArray(d) ? d : []); setLoading(false); })
      .catch(e => { setErr(e.message || "Failed to load history"); setLoading(false); });
  }, [rateId]);

  if (loading) return <div style={{ padding: "32px 0", textAlign: "center", color: "var(--ink-mute)", fontSize: 13 }}>Loading history…</div>;
  if (err)     return <div style={{ padding: "20px 0", color: "var(--red)", fontSize: 13 }}>{err}</div>;
  if (!history || history.length === 0)
    return <div style={{ padding: "32px 0", textAlign: "center", color: "var(--ink-mute)", fontSize: 13, fontStyle: "italic" }}>No history recorded yet.<br /><span style={{ fontSize: 11 }}>History is captured on every save or deactivation going forward.</span></div>;

  const BOOL_FIELDS = new Set(["is_invoicerate", "stcy_flag", "calculator"]);
  const fmtVal = (field, val) => {
    if (val === null || val === undefined) return null;
    if (BOOL_FIELDS.has(field)) {
      return ["true", "1", "1.0", "yes"].includes(String(val).toLowerCase()) ? "Yes" : "No";
    }
    return val;
  };

  const EVT_COLOR = { CREATE: "#22c55e", UPDATE: "var(--teal, #0d7e96)", DEACTIVATE: "#f59e0b", DELETE: "#ef4444" };
  const EVT_LABEL = { CREATE: "Created", UPDATE: "Updated", DEACTIVATE: "Deactivated", DELETE: "Deleted" };

  return (
    <div style={{ position: "relative", paddingLeft: 26 }}>
      {/* timeline spine */}
      <div style={{ position: "absolute", left: 8, top: 10, bottom: 10, width: 2, background: "var(--line)" }} />

      {history.map((entry, i) => {
        const color = EVT_COLOR[entry.event_type] || "#6b7280";
        const label = EVT_LABEL[entry.event_type] || entry.event_type;
        const ts = entry.changed_at ? entry.changed_at.slice(0, 16).replace("T", "  ") : "—";
        return (
          <div key={entry.id} style={{ position: "relative", marginBottom: 28 }}>
            {/* dot */}
            <div style={{ position: "absolute", left: -22, top: 4, width: 12, height: 12, borderRadius: "50%", background: color, border: "2.5px solid #fff", boxShadow: `0 0 0 1.5px ${color}40` }} />

            {/* header row */}
            <div style={{ display: "flex", alignItems: "baseline", gap: 8, marginBottom: 8, flexWrap: "wrap" }}>
              <span style={{ fontSize: 11, fontWeight: 700, color, textTransform: "uppercase", letterSpacing: "0.07em" }}>{label}</span>
              <span style={{ fontSize: 12, color: "var(--ink-mute)" }}>by</span>
              <span style={{ fontSize: 12, fontWeight: 600, color: "var(--ink)" }}>{entry.changed_by || "—"}</span>
              <span style={{ marginLeft: "auto", fontFamily: "'Maersk Text', ui-monospace, monospace", fontSize: 11, color: "var(--ink-mute)", whiteSpace: "nowrap" }}>{ts}</span>
            </div>

            {/* changes table */}
            {entry.changes && entry.changes.length > 0 ? (
              <div style={{ borderRadius: 6, border: "1px solid var(--line)", overflow: "hidden" }}>
                {entry.changes.map((c, j) => (
                  <div key={j} style={{
                    display: "grid", gridTemplateColumns: "130px 1fr 18px 1fr",
                    alignItems: "start", gap: "0 8px",
                    padding: "7px 12px", fontSize: 12,
                    background: j % 2 === 0 ? "#fff" : "var(--paper-2, #f7f8fa)",
                    borderTop: j > 0 ? "1px solid var(--line)" : "none",
                  }}>
                    <span style={{ color: "var(--ink-mute)", fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", paddingTop: 2 }}>{c.label}</span>
                    <span style={{ background: "#fee2e2", color: "#b91c1c", borderRadius: 3, padding: "2px 7px", wordBreak: "break-word", lineHeight: 1.4 }}>{fmtVal(c.field, c.old) ?? <em style={{ opacity: 0.5 }}>empty</em>}</span>
                    <span style={{ color: "var(--ink-mute)", textAlign: "center", paddingTop: 3 }}>→</span>
                    <span style={{ background: "#dcfce7", color: "#15803d", borderRadius: 3, padding: "2px 7px", wordBreak: "break-word", lineHeight: 1.4 }}>{fmtVal(c.field, c.new) ?? <em style={{ opacity: 0.5 }}>empty</em>}</span>
                  </div>
                ))}
              </div>
            ) : (
              <div style={{ fontSize: 12, color: "var(--ink-mute)", fontStyle: "italic" }}>
                {entry.event_type === "CREATE" ? "Rate record created with initial values." : "No field-level changes recorded."}
              </div>
            )}
          </div>
        );
      })}
    </div>
  );
}

function RateDetail({ rate, onClose, perms, onEdit, onDuplicate }) {
  if (!rate) return null;
  const [detailTab, setDetailTab] = useState("details");

  const today = new Date();
  const expiryDate = rate.expiry_date ? new Date(rate.expiry_date) : null;
  const effectiveDate = rate.effective_date ? new Date(rate.effective_date) : null;
  const daysToExpiry = expiryDate ? Math.ceil((expiryDate - today) / 86400000) : null;
  const totalDays = (expiryDate && effectiveDate) ? Math.ceil((expiryDate - effectiveDate) / 86400000) : null;
  const elapsed = (effectiveDate && effectiveDate <= today) ? Math.ceil((today - effectiveDate) / 86400000) : 0;
  const progress = totalDays > 0 ? Math.min(100, Math.max(0, (elapsed / totalDays) * 100)) : null;

  const expiryColor = daysToExpiry === null ? "rgba(207,224,242,0.4)"
    : daysToExpiry < 0 ? "#f87171"
    : daysToExpiry <= 14 ? "#f87171"
    : daysToExpiry <= 30 ? "#fbbf24"
    : "#4ade80";

  const progressBar = progress !== null ? (
    <div style={{ position: "relative", height: 3, background: "rgba(207,224,242,0.12)", borderRadius: 2, margin: "6px 0" }}>
      <div style={{ position: "absolute", left: 0, top: 0, height: "100%", width: `${progress}%`, background: `linear-gradient(90deg, #14a3bd, ${expiryColor})`, borderRadius: 2 }} />
    </div>
  ) : <div style={{ height: 3, background: "rgba(207,224,242,0.12)", borderRadius: 2, margin: "6px 0" }} />;

  const statusText = rate.status_name || rate.status || "";

  const RDSection = ({ label, children }) => (
    <div style={{ marginBottom: 22 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 14, paddingBottom: 8, borderBottom: "1px solid var(--line)" }}>
        <div style={{ width: 3, height: 14, background: "var(--teal)", borderRadius: 2, flexShrink: 0 }} />
        <span style={{ fontSize: 10, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.1em", color: "var(--ink-mute)" }}>{label}</span>
      </div>
      {children}
    </div>
  );

  const RDF = ({ label, value, mono, span2 }) => {
    const v = value == null || value === "" ? null : value;
    if (!v && v !== 0) return null;
    return (
      <div style={{ gridColumn: span2 ? "1 / -1" : undefined }}>
        <div style={{ fontSize: 10, color: "var(--ink-mute)", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.07em", marginBottom: 4 }}>{label}</div>
        <div style={{ fontSize: 13, fontFamily: mono ? "'Maersk Text', ui-monospace, monospace" : undefined, color: "var(--ink)", lineHeight: 1.4, wordBreak: "break-word" }}>{v}</div>
      </div>
    );
  };

  const FlagChip = ({ active, label }) => !active ? null : (
    <span style={{ display: "inline-flex", alignItems: "center", gap: 5, padding: "4px 12px", borderRadius: 20,
      background: "rgba(20,163,189,0.08)", color: "var(--teal)", fontSize: 12, fontWeight: 500,
      border: "1px solid rgba(20,163,189,0.2)" }}>
      <span style={{ width: 6, height: 6, borderRadius: "50%", background: "var(--teal)", display: "inline-block" }} />
      {label}
    </span>
  );

  const hasRoute = rate.route || rate.route_rate != null || rate.corridor_from || rate.corridor_to;
  const hasSlab = rate.slab_unit || rate.slab_type || rate.slab_min != null || rate.slab_max != null;
  const hasFlags = rate.is_invoicerate || rate.stcy_flag || rate.calculator;
  const currencyLabel = rate.currency_code || rate.iso_code || "";

  const rdTabStyle = (id) => ({
    padding: "7px 16px", fontSize: 13, background: "none", border: "none", cursor: "pointer",
    fontWeight: detailTab === id ? 600 : 400,
    color: detailTab === id ? "var(--teal)" : "var(--ink-mute)",
    borderBottom: detailTab === id ? "2px solid var(--teal)" : "2px solid transparent",
    whiteSpace: "nowrap",
  });

  return (
    <SlideOver open={true} onClose={onClose} wide title=""
      foot={
        <>
          {perms.editRates && <button className="btn" onClick={() => onDuplicate && onDuplicate(rate)}><Icon name="copy" size={14} /> Duplicate</button>}
          <div className="grow" />
          <button className="btn" onClick={onClose}>Close</button>
          {perms.editRates && <button className="btn primary" onClick={() => onEdit && onEdit(rate)}><Icon name="edit" size={14} /> Edit</button>}
        </>
      }
    >
      {/* ── HERO ── */}
      <div style={{
        margin: "-20px -20px 24px",
        padding: "20px 24px 22px",
        background: "linear-gradient(140deg, #0B1E33 0%, #0c3549 55%, #0d4a5e 100%)",
        position: "relative", overflow: "hidden",
      }}>
        <div style={{ position: "absolute", inset: 0, backgroundImage: "linear-gradient(rgba(20,163,189,0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(20,163,189,0.04) 1px, transparent 1px)", backgroundSize: "24px 24px", pointerEvents: "none" }} />

        {/* Row 1: ID · badges · status */}
        <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 14, position: "relative", flexWrap: "wrap" }}>
          <span style={{ fontFamily: "'Maersk Text', ui-monospace, monospace", fontSize: 11, color: "rgba(207,224,242,0.4)", letterSpacing: "0.05em" }}>#{rate.id}</span>
          {rate.source && <span style={{ fontSize: 10, padding: "2px 9px", borderRadius: 10, border: "1px solid rgba(20,163,189,0.35)", color: "#80d1de", fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.07em" }}>{rate.source}</span>}
          {rate.cost_category && <span style={{ fontSize: 10, padding: "2px 9px", borderRadius: 10, border: "1px solid rgba(207,224,242,0.18)", color: "rgba(207,224,242,0.55)", fontWeight: 600, textTransform: "uppercase" }}>{rate.cost_category}</span>}
          <div style={{ marginLeft: "auto" }}>
            <Pill status={statusText} />
          </div>
        </div>

        {/* Row 2: Cost type + terminal */}
        <div style={{ marginBottom: 16, position: "relative" }}>
          <div style={{ fontSize: 17, fontWeight: 700, color: "#cfe0f2", fontFamily: "'Maersk Headline', sans-serif", lineHeight: 1.25, marginBottom: 5 }}>{rate.cost_type_name}</div>
          <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <span style={{ fontSize: 13, color: "rgba(207,224,242,0.62)" }}>{rate.terminal_name}</span>
            {rate.terminal_code && <span style={{ fontFamily: "'Maersk Text', ui-monospace, monospace", fontSize: 11, color: "rgba(207,224,242,0.32)", background: "rgba(207,224,242,0.06)", padding: "1px 7px", borderRadius: 4 }}>{rate.terminal_code}</span>}
          </div>
        </div>

        {/* Row 3: Rate value */}
        <div style={{ display: "flex", alignItems: "baseline", gap: 10, marginBottom: 18, position: "relative" }}>
          <span style={{ fontSize: 40, fontWeight: 700, color: "#fff", fontFamily: "'Maersk Headline', sans-serif", letterSpacing: "-0.025em", lineHeight: 1 }}>{fmt.num(rate.rate_local)}</span>
          <span style={{ fontSize: 15, color: "#80d1de", fontWeight: 600 }}>{currencyLabel}</span>
          {rate.charge_unit_code && <span style={{ fontSize: 12, color: "rgba(207,224,242,0.35)", marginLeft: 2 }}>/ {rate.charge_unit_code}</span>}
        </div>

        {/* Row 4: Date range + progress */}
        <div style={{ position: "relative" }}>
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
            <span style={{ fontFamily: "'Maersk Text', ui-monospace, monospace", fontSize: 11, color: "rgba(207,224,242,0.65)" }}>{rate.effective_date?.slice(0,10) || "—"}</span>
            <span style={{ fontSize: 10, color: expiryColor, fontWeight: 500 }}>
              {daysToExpiry === null ? "Open-ended"
                : daysToExpiry < 0 ? `Expired ${Math.abs(daysToExpiry)}d ago`
                : daysToExpiry === 0 ? "Expires today"
                : `${daysToExpiry}d remaining`}
            </span>
            <span style={{ fontFamily: "'Maersk Text', ui-monospace, monospace", fontSize: 11, color: expiryDate ? expiryColor : "rgba(207,224,242,0.35)" }}>{rate.expiry_date?.slice(0,10) || "Open"}</span>
          </div>
          {progressBar}
        </div>
      </div>

      {/* ── TAB BAR ── */}
      <div style={{ display: "flex", borderBottom: "1px solid var(--line)", margin: "0 -20px 20px", padding: "0 20px" }}>
        <button style={rdTabStyle("details")} onClick={() => setDetailTab("details")}>Details</button>
        <button style={rdTabStyle("history")} onClick={() => setDetailTab("history")}>
          History
        </button>
      </div>

      {detailTab === "history" && <RateHistoryTab rateId={rate.id} />}

      {detailTab === "details" && <>

      {/* ── 1. TERMINAL / ROUTE ── */}
      <RDSection label="Terminal / Route">
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "14px 20px" }}>
          <div style={{ gridColumn: "1 / -1" }}>
            <div style={{ fontSize: 10, color: "var(--ink-mute)", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.07em", marginBottom: 4 }}>Terminal / Port</div>
            <div style={{ fontSize: 13, display: "flex", alignItems: "center", gap: 8 }}>
              <span style={{ fontWeight: 600 }}>{rate.terminal_name || "—"}</span>
              {rate.terminal_code && <span style={{ fontFamily: "'Maersk Text', ui-monospace, monospace", fontSize: 11, color: "var(--ink-mute)", background: "var(--paper-2, #f4f5f7)", padding: "1px 7px", borderRadius: 4 }}>{rate.terminal_code}</span>}
            </div>
          </div>
          <RDF label="Route"          value={rate.route}         span2 mono />
          <RDF label="Corridor From"  value={rate.corridor_from} />
          <RDF label="Corridor To"    value={rate.corridor_to} />
          <RDF label="Transport Type" value={rate.transport_type} />
          <RDF label="Route Rate"     value={rate.route_rate != null ? `${fmt.num(rate.route_rate)}  ${currencyLabel}` : null} />
        </div>
      </RDSection>

      {/* ── 2. CONTRACT INFO ── */}
      <RDSection label="Contract Info">
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "14px 20px" }}>
          <RDF label="Contract Reference"    value={rate.contract_ref}            mono span2 />
          <RDF label="Contract Owner"        value={rate.contract_owner}          span2 />
          <RDF label="Vendor (SAP Code)"     value={rate.sap_vendor_name ? `${rate.sap_vendor_name}  (${rate.sap_vendor_code})` : rate.sap_vendor_code} mono />
          <RDF label="Payment Terms"         value={rate.payment_terms} />
          <RDF label="Brand Company"         value={rate.brand_company_name || rate.brand_company_code} />
          <RDF label="Payable By"            value={rate.payable_by} />
          <RDF label="Contract Material"     value={rate.contract_material_name}  span2 />
        </div>
      </RDSection>

      {/* ── 3. COST TYPE & CONTAINER ── */}
      <RDSection label="Cost Type & Container">
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "14px 20px" }}>
          <RDF label="Cost Category"      value={rate.cost_category} />
          <RDF label="Rate Type"          value={rate.cost_type_name} />
          <RDF label="SAP Material Code"  value={rate.sap_material_code} mono />
          <RDF label="Material Name"      value={rate.material_name} />
          <RDF label="Shipment Type"      value={rate.shipment_type_code || "All"} />
          <RDF label="Container Type"     value={rate.containertype_code || "All"} />
          <RDF label="Container Size"     value={rate.container_size || "All"} />
          <RDF label="Container Fullness" value={rate.container_fullness || "All"} />
        </div>
      </RDSection>

      {/* ── 4. RATE ── */}
      <RDSection label="Rate">
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "14px 20px", marginBottom: 16 }}>
          <div>
            <div style={{ fontSize: 10, color: "var(--ink-mute)", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.07em", marginBottom: 4 }}>Rate (Local)</div>
            <div style={{ fontSize: 20, fontWeight: 700, color: "var(--ink)", letterSpacing: "-0.01em" }}>{fmt.num(rate.rate_local)} <span style={{ fontSize: 13, fontWeight: 500, color: "var(--ink-mute)" }}>{currencyLabel}</span></div>
          </div>
          <RDF label="Charge Unit"   value={rate.charge_unit_code} />
          <RDF label="Rate Status"   value={rate.status_name || rate.status} />
          <RDF label="Effective Date" value={rate.effective_date?.slice(0,10)} mono />
          <RDF label="Expiry Date"    value={rate.expiry_date?.slice(0,10) || "Open-ended"} mono />
          <div />
        </div>
        {/* Flags row */}
        <div style={{ display: "flex", gap: 8, flexWrap: "wrap", paddingTop: 10, borderTop: "1px solid var(--line)" }}>
          <FlagChip active={rate.is_invoicerate} label="Invoice Rate" />
          <FlagChip active={rate.stcy_flag}      label="STCY" />
          <FlagChip active={rate.calculator}     label="Calculator" />
          {!hasFlags && <span style={{ fontSize: 12, color: "var(--ink-mute)", fontStyle: "italic" }}>No flags set</span>}
        </div>
      </RDSection>

      {/* ── 5. SLAB / TIER (conditional) ── */}
      {hasSlab && (() => {
        const unit      = rate.slab_unit_code || "";
        const type      = rate.slab_type_code || "";
        const hasMin    = rate.slab_min != null;
        const hasMax    = rate.slab_max != null;
        const minVal    = hasMin ? fmt.num(rate.slab_min) : null;
        const maxVal    = hasMax ? fmt.num(rate.slab_max) : null;

        // Human-readable sentence
        let sentence = "This rate applies";
        if (unit) sentence += ` per ${unit}`;
        if (hasMin && hasMax)  sentence += ` when quantity is between ${minVal} and ${maxVal}`;
        else if (hasMin)       sentence += ` when quantity is ≥ ${minVal} (no upper limit)`;
        else if (hasMax)       sentence += ` when quantity is ≤ ${maxVal}`;
        if (type) sentence += ` · ${type} pricing`;
        sentence += ".";

        return (
          <RDSection label="Slab / Tier">
            {/* Summary sentence */}
            <div style={{ fontSize: 12.5, color: "var(--ink-mute)", fontStyle: "italic", marginBottom: 16, lineHeight: 1.6 }}>
              {sentence}
            </div>

            {/* Visual range bar */}
            {(hasMin || hasMax) && (
              <div style={{ marginBottom: 16 }}>
                <div style={{ position: "relative", height: 28, display: "flex", alignItems: "center" }}>
                  {/* track */}
                  <div style={{ position: "absolute", left: 0, right: 0, height: 6, borderRadius: 3, background: "var(--line)" }} />
                  {/* filled portion */}
                  <div style={{
                    position: "absolute",
                    left:  hasMin ? "20%" : "0%",
                    right: hasMax ? "20%" : "0%",
                    height: 6, borderRadius: 3,
                    background: "linear-gradient(90deg, var(--teal, #0d7e96), #22c55e)",
                  }} />
                  {/* left cap / arrow */}
                  {hasMin ? (
                    <div style={{ position: "absolute", left: "20%", width: 12, height: 12, borderRadius: "50%", background: "var(--teal, #0d7e96)", border: "2px solid #fff", boxShadow: "0 0 0 1.5px var(--teal,#0d7e96)", transform: "translateX(-50%)" }} />
                  ) : (
                    <div style={{ position: "absolute", left: 0, fontSize: 16, color: "var(--teal,#0d7e96)", lineHeight: 1, transform: "translateX(-4px)" }}>←</div>
                  )}
                  {/* right cap / arrow */}
                  {hasMax ? (
                    <div style={{ position: "absolute", left: "80%", width: 12, height: 12, borderRadius: "50%", background: "#22c55e", border: "2px solid #fff", boxShadow: "0 0 0 1.5px #22c55e", transform: "translateX(-50%)" }} />
                  ) : (
                    <div style={{ position: "absolute", right: 0, fontSize: 16, color: "#22c55e", lineHeight: 1 }}>→</div>
                  )}
                </div>
                {/* labels under bar */}
                <div style={{ display: "flex", justifyContent: "space-between", marginTop: 4 }}>
                  <div style={{ fontSize: 11, color: "var(--ink-mute)", paddingLeft: hasMin ? "20%" : 0, transform: "translateX(-50%)" }}>
                    {hasMin ? <><span style={{ fontWeight: 600, color: "var(--teal,#0d7e96)" }}>{minVal}</span><br /><span style={{ opacity: .7 }}>Min</span></> : <span style={{ opacity: .5 }}>0</span>}
                  </div>
                  <div style={{ fontSize: 11, color: "var(--ink-mute)", textAlign: "right", paddingRight: hasMax ? "20%" : 0, transform: "translateX(50%)" }}>
                    {hasMax ? <><span style={{ fontWeight: 600, color: "#15803d" }}>{maxVal}</span><br /><span style={{ opacity: .7 }}>Max</span></> : <span style={{ opacity: .5 }}>∞</span>}
                  </div>
                </div>
              </div>
            )}

            {/* Meta badges */}
            <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
              {unit && (
                <div style={{ display: "flex", flexDirection: "column", background: "var(--paper-2,#f7f8fa)", border: "1px solid var(--line)", borderRadius: 6, padding: "6px 12px", minWidth: 80 }}>
                  <span style={{ fontSize: 10, color: "var(--ink-mute)", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 2 }}>Unit</span>
                  <span style={{ fontSize: 13, fontWeight: 600, color: "var(--ink)" }}>{unit}</span>
                </div>
              )}
              {type && (
                <div style={{ display: "flex", flexDirection: "column", background: "var(--paper-2,#f7f8fa)", border: "1px solid var(--line)", borderRadius: 6, padding: "6px 12px", minWidth: 80 }}>
                  <span style={{ fontSize: 10, color: "var(--ink-mute)", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 2 }}>Type</span>
                  <span style={{ fontSize: 13, fontWeight: 600, color: "var(--ink)" }}>{type}</span>
                </div>
              )}
              {hasMin && (
                <div style={{ display: "flex", flexDirection: "column", background: "#eff6ff", border: "1px solid #bfdbfe", borderRadius: 6, padding: "6px 12px", minWidth: 80 }}>
                  <span style={{ fontSize: 10, color: "#3b82f6", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 2 }}>Min</span>
                  <span style={{ fontSize: 13, fontWeight: 600, color: "#1d4ed8" }}>{minVal}</span>
                </div>
              )}
              {hasMax ? (
                <div style={{ display: "flex", flexDirection: "column", background: "#f0fdf4", border: "1px solid #bbf7d0", borderRadius: 6, padding: "6px 12px", minWidth: 80 }}>
                  <span style={{ fontSize: 10, color: "#16a34a", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 2 }}>Max</span>
                  <span style={{ fontSize: 13, fontWeight: 600, color: "#15803d" }}>{maxVal}</span>
                </div>
              ) : hasMin && (
                <div style={{ display: "flex", flexDirection: "column", background: "#f0fdf4", border: "1px solid #bbf7d0", borderRadius: 6, padding: "6px 12px", minWidth: 80 }}>
                  <span style={{ fontSize: 10, color: "#16a34a", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 2 }}>Max</span>
                  <span style={{ fontSize: 13, fontWeight: 600, color: "#15803d" }}>∞</span>
                </div>
              )}
            </div>
          </RDSection>
        );
      })()}

      {/* ── 6. OTHERS ── */}
      <RDSection label="Others">
        {rate.notes ? (
          <div style={{ fontSize: 13, color: "var(--ink)", lineHeight: 1.65, background: "var(--paper-2, #f7f8fa)", padding: "12px 16px", borderRadius: 6, whiteSpace: "pre-wrap", borderLeft: "3px solid var(--line)", marginBottom: 16 }}>
            {rate.notes}
          </div>
        ) : (
          <div style={{ fontSize: 12, color: "var(--ink-mute)", fontStyle: "italic", marginBottom: 16 }}>No notes</div>
        )}
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "12px 20px" }}>
          <div>
            <div style={{ fontSize: 10, color: "var(--ink-mute)", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.07em", marginBottom: 4 }}>Created By</div>
            <div style={{ fontSize: 13 }}>{rate.created_by || "—"}</div>
            <div style={{ fontFamily: "'Maersk Text', ui-monospace, monospace", fontSize: 11, color: "var(--ink-mute)", marginTop: 3 }}>{rate.created_at?.slice(0,16)?.replace("T"," ") || ""}</div>
          </div>
          <div>
            <div style={{ fontSize: 10, color: "var(--ink-mute)", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.07em", marginBottom: 4 }}>Modified By</div>
            <div style={{ fontSize: 13 }}>{rate.modified_by || "—"}</div>
            <div style={{ fontFamily: "'Maersk Text', ui-monospace, monospace", fontSize: 11, color: "var(--ink-mute)", marginTop: 3 }}>{rate.modified_at?.slice(0,16)?.replace("T"," ") || ""}</div>
          </div>
        </div>
      </RDSection>

      </>}
    </SlideOver>
  );
}

// Searchable select dropdown — works for both object arrays and string arrays
function SearchSelect({ options, value, onChange, placeholder, getLabel, getValue }) {
  const [search, setSearch] = useState("");
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  const listRef = useRef(null);

  const gl = getLabel || (o => String(o));
  const gv = getValue || (o => String(o));

  // Normalize: lowercase + strip non-alphanumeric (handles "-", "–", "/", etc.)
  const norm = s => s.toLowerCase().replace(/[^a-z0-9一-鿿]+/g, " ").replace(/\s+/g, " ").trim();
  const filtered = options.filter(o => {
    if (!search) return true;
    const label = norm(gl(o));
    // Every search token must appear somewhere in the label
    return norm(search).split(" ").filter(Boolean).every(t => label.includes(t));
  });
  const selected = options.find(o => String(gv(o)) === String(value));
  const displayLabel = selected ? gl(selected) : (value || "");

  useEffect(() => {
    const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", handler);
    return () => document.removeEventListener("mousedown", handler);
  }, []);

  // Auto-scroll to selected item when dropdown opens
  useEffect(() => {
    if (open && listRef.current && value) {
      const selectedEl = listRef.current.querySelector("[data-selected='true']");
      if (selectedEl) selectedEl.scrollIntoView({ block: "nearest" });
    }
  }, [open]);

  return (
    <div ref={ref} style={{ position: "relative" }}>
      <input
        className="input"
        value={open ? search : displayLabel}
        onFocus={() => { setOpen(true); setSearch(""); }}
        onChange={e => setSearch(e.target.value)}
        placeholder={placeholder || "Search…"}
      />
      {open && (
        <div ref={listRef} style={{ position: "absolute", top: "100%", left: 0, right: 0, background: "var(--paper)", border: "1px solid var(--line)", borderRadius: 5, zIndex: 400, maxHeight: 220, overflowY: "auto", boxShadow: "0 4px 12px rgba(0,0,0,.15)" }}>
          {filtered.length === 0
            ? <div style={{ padding: "8px 12px", color: "var(--ink-muted)", fontSize: 13 }}>No match</div>
            : filtered.map((o, i) => {
              const isSelected = String(gv(o)) === String(value);
              return (
                <div key={i}
                  data-selected={isSelected ? "true" : undefined}
                  style={{
                    padding: "7px 12px", cursor: "pointer", fontSize: 13,
                    background: isSelected ? "#d4f0f5" : "transparent",
                    fontWeight: isSelected ? 600 : 400,
                    color: isSelected ? "var(--teal, #0d7e96)" : "inherit",
                    borderLeft: isSelected ? "3px solid var(--teal, #0d7e96)" : "3px solid transparent",
                  }}
                  onMouseDown={() => { onChange(gv(o)); setSearch(""); setOpen(false); }}
                  onMouseEnter={e => { if (!isSelected) e.currentTarget.style.background = "var(--paper-2, #eef1f5)"; }}
                  onMouseLeave={e => { e.currentTarget.style.background = isSelected ? "#d4f0f5" : "transparent"; }}
                >
                  {gl(o)}
                </div>
              );
            })
          }
        </div>
      )}
    </div>
  );
}

const COST_CATEGORIES = ["CYB", "CYM", "PE"];

function AddRateForm({ onClose, onSave, prefill }) {
  const today = new Date().toISOString().slice(0, 10);
  const isEdit = !!(prefill && prefill.id);

  const initForm = (p) => {
    if (!p) return {
      terminal_id: "", cost_type_id: "", cost_category: "", charge_unit_id: "", containertype: "",
      container_size: "", container_fullness: "", shipment_type: "",
      rate_local: "", primary_currency: "",
      effective_date: today, expiry_date: "",
      sap_material_code: "",
      contract_ref: "", contract_owner: "",
      transport_type: "",
      route: "", route_rate: "", corridor_from: "", corridor_to: "",
      sap_vendor_code: "", payment_terms: "", brand_company_id: "", payable_by: "Maersk",
      slab_unit: "", slab_type: "", slab_min: "", slab_max: "",
      is_invoicerate: false, stcy_flag: false, calculator: false,
      notes: "",
      status: "Active",
    };
    return {
      terminal_id: String(p.terminal_id || ""),
      cost_type_id: String(p.cost_type_id || ""),
      cost_category: p.cost_category || "",
      charge_unit_id: p.charge_unit_id != null ? String(p.charge_unit_id) : "",
      containertype: p.containertype != null ? String(p.containertype) : "",
      container_size: p.container_size || "",
      container_fullness: p.container_fullness || "",
      shipment_type: p.shipment_type != null ? String(p.shipment_type) : "",
      rate_local: p.rate_local != null ? String(p.rate_local) : "",
      primary_currency: p.primary_currency != null ? String(p.primary_currency) : "",
      effective_date: p.effective_date ? p.effective_date.slice(0, 10) : today,
      expiry_date: p.expiry_date ? p.expiry_date.slice(0, 10) : "",
      sap_material_code: p.sap_material_code || "",
      contract_ref: p.contract_ref || "",
      contract_owner: p.contract_owner || "",
      transport_type: p.transport_type || "",
      route: p.route || "",
      route_rate: p.route_rate != null ? String(p.route_rate) : "",
      corridor_from: p.corridor_from || "",
      corridor_to: p.corridor_to || "",
      sap_vendor_code: p.sap_vendor_code || "",
      payment_terms: p.payment_terms || "",
      brand_company_id: p.brand_company_id != null ? String(p.brand_company_id) : "",
      payable_by: p.payable_by || "Maersk",
      slab_unit: p.slab_unit != null ? String(p.slab_unit) : "",
      slab_type: p.slab_type != null ? String(p.slab_type) : "",
      slab_min: p.slab_min != null ? String(p.slab_min) : "",
      slab_max: p.slab_max != null ? String(p.slab_max) : "",
      is_invoicerate: !!p.is_invoicerate,
      stcy_flag: !!p.stcy_flag,
      calculator: !!p.calculator,
      notes: p.notes || "",
      status: p.status || "Active",
      status_id: p.status_id != null ? String(parseInt(p.status_id)) : "",
    };
  };

  const [form, setForm] = useState(() => initForm(prefill));
  const [terminals, setTerminals] = useState([]);
  const [costTypes, setCostTypes] = useState([]);
  const [chargeUnits, setChargeUnits] = useState([]);
  const [equipmentTypes, setCargoTypes] = useState([]);
  const [currencies, setCurrencies] = useState([]);
  const [vendors, setVendors] = useState([]);
  const [brandCompanies, setBrandCompanies] = useState([]);
  const [materials, setMaterials] = useState([]);
  const [paymentTerms, setPaymentTerms] = useState([]);
  const [moveTypes, setMoveTypes] = useState([]);
  const [slabUnits, setSlabUnits] = useState([]);
  const [slabTypes, setSlabTypes] = useState([]);
  const [rateStatuses, setRateStatuses] = useState([]);
  const [violations, setViolations] = useState([]);
  const [saving, setSaving] = useState(false);
  const [saveError, setSaveError] = useState(null);
  const [activeTab, setActiveTab] = useState(0);
  const [showCorridor, setShowCorridor] = useState(false);
  const [materialSuggestions, setMaterialSuggestions] = useState([]);

  // Build field suggestions from ocms_material structured fields
  const parseMaterial = (material, currentForm) => {
    if (!material) return [];
    const suggestions = [];

    // cost_type_name → match against costTypes by name or code
    if (material.cost_type_name) {
      const n = material.cost_type_name.toLowerCase().trim();
      const matchedCT = costTypes.find(ct =>
        ct.name.toLowerCase() === n ||
        ct.code.toLowerCase() === n ||
        ct.name.toLowerCase().includes(n) ||
        n.includes(ct.name.toLowerCase())
      );
      if (matchedCT && String(matchedCT.id) !== String(currentForm.cost_type_id)) {
        suggestions.push({
          field: "cost_type_id", label: "Rate Type",
          suggested: String(matchedCT.id), suggestedLabel: matchedCT.name,
          currentLabel: costTypes.find(c => String(c.id) === String(currentForm.cost_type_id))?.name || "—",
        });
      }
    }

    // containertype_code → exact code match against equipment types
    if (material.containertype_code) {
      const matchedET = equipmentTypes.find(e => e.code === material.containertype_code);
      if (matchedET && String(matchedET.id) !== String(currentForm.containertype)) {
        suggestions.push({
          field: "containertype", label: "Container Type",
          suggested: String(matchedET.id), suggestedLabel: matchedET.name,
          currentLabel: equipmentTypes.find(e => String(e.id) === String(currentForm.containertype))?.name || "All",
        });
      }
    }

    // container_size → direct value
    if (material.container_size && material.container_size !== currentForm.container_size) {
      suggestions.push({
        field: "container_size", label: "Container Size",
        suggested: material.container_size, suggestedLabel: material.container_size,
        currentLabel: currentForm.container_size || "All",
      });
    }

    // container_fullness → direct value
    if (material.container_fullness && material.container_fullness !== currentForm.container_fullness) {
      suggestions.push({
        field: "container_fullness", label: "Container Fullness",
        suggested: material.container_fullness, suggestedLabel: material.container_fullness,
        currentLabel: currentForm.container_fullness || "All",
      });
    }

    return suggestions;
  };

  const handleMaterialChange = (code) => {
    const material = materials.find(m => m.code === code);
    const newForm = { ...form, sap_material_code: code };
    setForm(newForm);
    setMaterialSuggestions(material ? parseMaterial(material, newForm) : []);
  };

  const applyAllSuggestions = () => {
    const patch = {};
    materialSuggestions.forEach(s => { patch[s.field] = s.suggested; });
    setForm(f => ({ ...f, ...patch }));
    setMaterialSuggestions([]);
  };

  const applySuggestion = (s) => {
    setForm(f => ({ ...f, [s.field]: s.suggested }));
    setMaterialSuggestions(prev => prev.filter(x => x.field !== s.field));
  };

  useEffect(() => {
    Promise.all([
      apiFetch("/api/metadata/terminals/scoped"),
      apiFetchMeta("cost-types"),
      apiFetchMeta("charge-units"),
      apiFetchMeta("equipment-types"),
      apiFetchMeta("currencies"),
      apiFetchMeta("vendors").catch(() => []),
      apiFetchMeta("brand-companies").catch(() => []),
      apiFetchMeta("materials").catch(() => []),
      apiFetchMeta("payment-terms").catch(() => []),
      apiFetchMeta("move-types").catch(() => []),
      apiFetchMeta("slab-units").catch(() => []),
      apiFetchMeta("slab-types").catch(() => []),
      apiFetchMeta("rate-statuses").catch(() => []),
    ]).then(([terms, cts, cus, cgts, curs, vds, bcs, mats, pts, mvts, sus, sts, rss]) => {
      setTerminals(Array.isArray(terms) ? terms : []);
      setCostTypes(Array.isArray(cts) ? cts : []);
      setChargeUnits(Array.isArray(cus) ? cus : []);
      setCargoTypes(Array.isArray(cgts) ? cgts : []);
      setCurrencies(Array.isArray(curs) ? curs : []);
      setVendors(Array.isArray(vds) ? vds : []);
      setBrandCompanies(Array.isArray(bcs) ? bcs : []);
      setMaterials(Array.isArray(mats) ? mats : []);
      setPaymentTerms(Array.isArray(pts) ? pts : []);
      setMoveTypes(Array.isArray(mvts) ? mvts : []);
      setSlabUnits(Array.isArray(sus) ? sus : []);
      setSlabTypes(Array.isArray(sts) ? sts : []);
      const rssArr = Array.isArray(rss) ? rss : [];
      setRateStatuses(rssArr);
      // If prefill provided a status name but no status_id, resolve the id now
      setForm(f => {
        if (!f.status_id && f.status) {
          const match = rssArr.find(s => s.name === f.status);
          if (match) return { ...f, status_id: String(match.id) };
        }
        return f;
      });
    });
  }, []);

  const submit = async (asDraft = false) => {
    if (!asDraft && showCorridor) {
      const missing = [];
      if (!form.corridor_from?.trim()) missing.push("Corridor From");
      if (!form.corridor_to?.trim())   missing.push("Corridor To");
      if (!form.transport_type)        missing.push("Transport Type");
      if (missing.length) {
        setSaveError(`Corridor info is enabled — please fill in: ${missing.join(", ")}.`);
        setActiveTab(0);
        return;
      }
    }
    if (!asDraft && form.slab_min !== "" && form.slab_max !== "") {
      if (parseFloat(form.slab_max) < parseFloat(form.slab_min)) {
        setSaveError("Slab Max must be greater than or equal to Slab Min.");
        setActiveTab(4);
        return;
      }
    }
    setSaving(true); setSaveError(null); setViolations([]);
    try {
      const body = {
        ...form,
        terminal_id: parseInt(form.terminal_id),
        cost_type_id: parseInt(form.cost_type_id),
        charge_unit_id: form.charge_unit_id ? parseInt(form.charge_unit_id) : null,
        containertype: form.containertype ? parseInt(form.containertype) : null,
        shipment_type: form.shipment_type ? parseInt(form.shipment_type) : null,
        primary_currency: form.primary_currency ? parseInt(form.primary_currency) : null,
        rate_local: parseFloat(form.rate_local),
        route_rate: form.route_rate !== "" ? parseFloat(form.route_rate) : null,
        slab_min: form.slab_min !== "" ? parseFloat(form.slab_min) : null,
        slab_max: form.slab_max !== "" ? parseFloat(form.slab_max) : null,
        expiry_date: form.expiry_date || null,
        slab_unit: form.slab_unit ? parseInt(form.slab_unit) : null,
        slab_type: form.slab_type ? parseInt(form.slab_type) : null,
        brand_company_id: form.brand_company_id ? parseInt(form.brand_company_id) : null,
        payable_by: form.payable_by || "Maersk",
        status: asDraft ? "Draft" : (rateStatuses.find(s => String(s.id) === form.status_id)?.name || form.status || "Active"),
        ...(!isEdit && { source: "Manual" }),
      };
      if (isEdit) {
        await apiFetch(`/api/rates/${prefill.id}`, { method: "PUT", body: JSON.stringify(body) });
        onSave("Rate updated successfully");
      } else {
        await apiFetch("/api/rates", { method: "POST", body: JSON.stringify(body) });
        onSave("Rate created successfully");
      }
    } catch (e) {
      if (e.violations) setViolations(e.violations);
      else setSaveError(e.message || "Failed to save rate");
    }
    setSaving(false);
  };

  const TABS = [
    "Terminal/Corridor", "Contract Info", "Cost Type & Container",
    "Rate", "Slab/Tier", "Others"
  ];

  const tabStyle = i => ({
    padding: "7px 14px",
    fontSize: 13,
    fontWeight: activeTab === i ? 600 : 400,
    color: activeTab === i ? "var(--teal)" : "var(--ink-muted)",
    borderTop: "none",
    borderLeft: "none",
    borderRight: "none",
    borderBottom: activeTab === i ? "2px solid var(--teal)" : "2px solid transparent",
    background: "none",
    cursor: "pointer",
    whiteSpace: "nowrap",
  });

  return (
    <SlideOver
      open={true}
      onClose={onClose}
      wide
      title={isEdit ? "Edit rate" : "Add new rate"}
      sub={isEdit ? `Editing rate #${prefill.id}` : "Manual entry · 6 sections"}
      foot={
        <>
          <button className="btn ghost" onClick={onClose}>Cancel</button>
          {!isEdit && <button className="btn" onClick={() => submit(true)} disabled={saving}>Save as Draft</button>}
          <button className="btn primary" onClick={() => submit(false)} disabled={saving || !form.terminal_id || !form.cost_type_id || !form.rate_local}>
            <Icon name="check" size={14} /> {saving ? "Saving…" : isEdit ? "Update rate" : "Create rate"}
          </button>
        </>
      }
    >
      {/* Tab bar */}
      <div style={{ display: "flex", borderBottom: "1px solid var(--line)", marginBottom: 18, overflowX: "auto", gap: 0 }}>
        {TABS.map((t, i) => (
          <button key={i} style={tabStyle(i)} onClick={() => setActiveTab(i)}>{t}</button>
        ))}
      </div>

      {/* Tab 0 — Terminal / Corridor */}
      {activeTab === 0 && (
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }}>
          <Field label="Terminal / Port" required span={2}>
            <SearchSelect
              options={terminals}
              value={form.terminal_id}
              onChange={v => setForm({ ...form, terminal_id: v })}
              placeholder="Search terminal…"
              getLabel={t => `${t.name} (${t.code})`}
              getValue={t => String(t.id)}
            />
          </Field>

          {/* Corridor toggle */}
          <div style={{ gridColumn: "1 / -1", display: "flex", alignItems: "center", gap: 10, padding: "6px 0 2px" }}>
            <button
              type="button"
              onClick={() => setShowCorridor(v => !v)}
              style={{
                position: "relative", width: 36, height: 20, borderRadius: 10, border: "none", cursor: "pointer",
                background: showCorridor ? "var(--teal, #0d7e96)" : "#cbd5e1",
                transition: "background 0.2s",
                flexShrink: 0,
              }}
            >
              <span style={{
                position: "absolute", top: 3, left: showCorridor ? 18 : 3, width: 14, height: 14,
                borderRadius: "50%", background: "#fff", boxShadow: "0 1px 3px rgba(0,0,0,.2)",
                transition: "left 0.2s",
              }} />
            </button>
            <span style={{ fontSize: 13, color: "var(--ink-mute)", userSelect: "none" }}>
              {showCorridor ? "Corridor / Route info enabled" : "Enable Corridor / Route info"}
            </span>
          </div>

          {showCorridor && <>
            <Field label="Route" span={2}>
              <input className="input" placeholder="e.g. SHA-LAX" value={form.route} onChange={e => setForm({ ...form, route: e.target.value })} />
            </Field>
            <Field label="Corridor From" required>
              <input className="input" placeholder="Origin corridor" value={form.corridor_from} onChange={e => setForm({ ...form, corridor_from: e.target.value })} />
            </Field>
            <Field label="Corridor To" required>
              <input className="input" placeholder="Destination corridor" value={form.corridor_to} onChange={e => setForm({ ...form, corridor_to: e.target.value })} />
            </Field>
            <Field label="Transport Type" required>
              <select className="select" value={form.transport_type} onChange={e => setForm({ ...form, transport_type: e.target.value })}>
                <option value="">Select…</option>
                <option>Free</option><option>Rail</option><option>Barge</option><option>Road</option><option>Sea</option>
              </select>
            </Field>
          </>}
        </div>
      )}

      {/* Tab 1 — Contract Info */}
      {activeTab === 1 && (
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }}>
          <Field label="Brand Company" span={2}>
            <SearchSelect
              options={brandCompanies}
              value={form.brand_company_id}
              onChange={v => setForm({ ...form, brand_company_id: v })}
              placeholder="Search brand company…"
              getLabel={o => o.name}
              getValue={o => String(o.id)}
            />
          </Field>
          <Field label="Contract Reference" span={2}>
            <input className="input mono" placeholder="CTR-APA-2025-XXX" value={form.contract_ref} onChange={e => setForm({ ...form, contract_ref: e.target.value })} />
          </Field>
          <Field label="Contract Owner" span={2}>
            <input className="input" placeholder="Owner name" value={form.contract_owner} onChange={e => setForm({ ...form, contract_owner: e.target.value })} />
          </Field>
          <Field label="Vendor Name" span={2}>
            <SearchSelect
              options={vendors}
              value={form.sap_vendor_code}
              onChange={v => setForm({ ...form, sap_vendor_code: v })}
              placeholder="Search vendor…"
              getLabel={v => `${v.name} (${v.code})`}
              getValue={v => v.code}
            />
          </Field>
          <Field label="Payment Terms" span={2}>
            <SearchSelect
              options={paymentTerms}
              value={form.payment_terms}
              onChange={v => setForm({ ...form, payment_terms: v })}
              placeholder="Search payment terms…"
              getLabel={p => p.name}
              getValue={p => p.name}
            />
          </Field>
        </div>
      )}

      {/* Tab 2 — Cost Type & Container Info */}
      {activeTab === 2 && (
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }}>
        {materialSuggestions.length > 0 && (
          <div style={{ gridColumn: "1 / -1", background: "#fffbeb", border: "1px solid #f0c84a", borderRadius: 8, padding: "12px 16px", marginBottom: 4 }}>
            <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 10 }}>
              <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                <Icon name="sparkle" size={15} stroke="#b45309" />
                <span style={{ fontWeight: 600, fontSize: 13, color: "#92400e" }}>Material suggestions — review differences</span>
              </div>
              <div style={{ display: "flex", gap: 8 }}>
                <button className="btn ghost tiny" style={{ fontSize: 12 }} onClick={() => setMaterialSuggestions([])}>Dismiss</button>
                <button className="btn primary" style={{ fontSize: 12, padding: "4px 12px" }} onClick={applyAllSuggestions}>Apply All</button>
              </div>
            </div>
            <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
              {materialSuggestions.map(s => (
                <div key={s.field} style={{ display: "flex", alignItems: "center", gap: 10, fontSize: 13, background: "#fff", borderRadius: 6, padding: "7px 12px", border: "1px solid #f0c84a" }}>
                  <span style={{ color: "#6b7280", minWidth: 130 }}>{s.label}</span>
                  <span style={{ background: "#fee2e2", color: "#b91c1c", borderRadius: 4, padding: "1px 8px", fontWeight: 500 }}>{s.currentLabel}</span>
                  <span style={{ color: "#9ca3af" }}>→</span>
                  <span style={{ background: "#dcfce7", color: "#15803d", borderRadius: 4, padding: "1px 8px", fontWeight: 600 }}>{s.suggestedLabel}</span>
                  <button className="btn ghost tiny" style={{ marginLeft: "auto", fontSize: 11, padding: "2px 10px" }} onClick={() => applySuggestion(s)}>Apply</button>
                </div>
              ))}
            </div>
          </div>
        )}
          <Field label="Cost Category">
            <select className="select" value={form.cost_category} onChange={e => setForm({ ...form, cost_category: e.target.value })}>
              <option value="">Select…</option>
              {COST_CATEGORIES.map(c => <option key={c} value={c}>{c}</option>)}
            </select>
          </Field>
          <Field label="Rate Type" required>
            <SearchSelect
              options={costTypes}
              value={form.cost_type_id}
              onChange={v => setForm({ ...form, cost_type_id: v, charge_unit_id: "" })}
              placeholder="Search rate type…"
              getLabel={c => c.name}
              getValue={c => String(c.id)}
            />
          </Field>
          <Field label="SAP Material Code" hint="From ocms_material">
            <SearchSelect
              options={materials}
              value={form.sap_material_code}
              onChange={handleMaterialChange}
              placeholder="Search material…"
              getLabel={m => `${m.name} (${m.code})`}
              getValue={m => m.code}
            />
          </Field>
          <Field label="Shipment Type">
            <select className="select" value={form.shipment_type} onChange={e => setForm({ ...form, shipment_type: e.target.value })}>
              <option value="">All / Any</option>
              {moveTypes.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
            </select>
          </Field>
          <Field label="Container Type" span={2}>
            <select className="select" value={form.containertype} onChange={e => setForm({ ...form, containertype: e.target.value })}>
              <option value="">All / Any</option>
              {equipmentTypes.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
            </select>
          </Field>
          <Field label="Container Size">
            <select className="select" value={form.container_size} onChange={e => setForm({ ...form, container_size: e.target.value })}>
              <option value="">All</option>
              <option>20</option><option>40</option><option>45</option>
            </select>
          </Field>
          <Field label="Container Fullness">
            <select className="select" value={form.container_fullness} onChange={e => setForm({ ...form, container_fullness: e.target.value })}>
              <option value="">All</option>
              <option>Full</option><option>Empty</option>
            </select>
          </Field>
        </div>
      )}

      {/* Tab 3 — Rate */}
      {activeTab === 3 && (
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }}>
          <Field label="Rate (Local currency)" required>
            <input className="input mono" placeholder="0.00" value={form.rate_local} onChange={e => setForm({ ...form, rate_local: e.target.value })} />
          </Field>
          <Field label="Primary Currency">
            <SearchSelect
              options={currencies}
              value={form.primary_currency}
              onChange={v => setForm({ ...form, primary_currency: v })}
              placeholder="Search currency…"
              getLabel={c => `${c.iso_code}${c.name ? " – " + c.name : ""}`}
              getValue={c => String(c.id)}
            />
          </Field>
          <Field label="Charge Unit">
            <select className="select" value={form.charge_unit_id} onChange={e => setForm({ ...form, charge_unit_id: e.target.value })}>
              <option value="">Any / All</option>
              {chargeUnits.map(u => <option key={u.id} value={u.id}>{u.name}</option>)}
            </select>
          </Field>
          <Field label="Rate Status">
            <select className="select" value={form.status_id || ""} onChange={e => setForm({ ...form, status_id: e.target.value })}>
              {rateStatuses.length > 0
                ? rateStatuses.map(s => <option key={s.id} value={String(s.id)}>{s.name}</option>)
                : RATE_STATUSES.map(s => <option key={s} value={s}>{s}</option>)}
            </select>
          </Field>
          <Field label="Effective Date" required>
            <input className="input mono" type="date" value={form.effective_date} onChange={e => setForm({ ...form, effective_date: e.target.value })} />
          </Field>
          <Field label="Expiry Date" hint="Leave empty for open-ended">
            <input className="input mono" type="date" value={form.expiry_date} onChange={e => setForm({ ...form, expiry_date: e.target.value })} />
          </Field>
          <Field label="Is Invoice Rate" span={2}>
            <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", userSelect: "none" }}>
              <input type="checkbox" checked={!!form.is_invoicerate} onChange={e => setForm({ ...form, is_invoicerate: e.target.checked })} style={{ width: 16, height: 16, accentColor: "var(--teal)" }} />
              <span style={{ fontSize: 13 }}>This rate is used for invoice billing</span>
            </label>
          </Field>
          <Field label="STCY Flag" span={2}>
            <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", userSelect: "none" }}>
              <input type="checkbox" checked={!!form.stcy_flag} onChange={e => setForm({ ...form, stcy_flag: e.target.checked })} style={{ width: 16, height: 16, accentColor: "var(--teal)" }} />
              <span style={{ fontSize: 13 }}>This rate is used for MEPC Cost calculation</span>
            </label>
          </Field>
          <Field label="Calculator" span={2}>
            <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", userSelect: "none" }}>
              <input type="checkbox" checked={!!form.calculator} onChange={e => setForm({ ...form, calculator: e.target.checked })} style={{ width: 16, height: 16, accentColor: "var(--teal)" }} />
              <span style={{ fontSize: 13 }}>Include in cost calculator</span>
            </label>
          </Field>
          <Field label="Payable By" span={2}>
            <select className="select" value={form.payable_by} onChange={e => setForm({ ...form, payable_by: e.target.value })}>
              <option value="Maersk">Maersk</option>
              <option value="Customer">Customer</option>
            </select>
          </Field>
        </div>
      )}

      {/* Tab 4 — Slab / Tier */}
      {activeTab === 4 && (
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }}>
          <Field label="Slab Unit">
            <select className="input" value={form.slab_unit} onChange={e => setForm({ ...form, slab_unit: e.target.value })}>
              <option value="">— Select —</option>
              {slabUnits.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
            </select>
          </Field>
          <Field label="Slab Type">
            <select className="input" value={form.slab_type} onChange={e => setForm({ ...form, slab_type: e.target.value })}>
              <option value="">— Select —</option>
              {slabTypes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
            </select>
          </Field>
          <Field label="Slab Min">
            <input className="input mono" type="number" placeholder="0" value={form.slab_min} onChange={e => setForm({ ...form, slab_min: e.target.value })} />
          </Field>
          <Field label="Slab Max">
            <input className="input mono" type="number" placeholder="0" value={form.slab_max} onChange={e => setForm({ ...form, slab_max: e.target.value })} />
          </Field>
        </div>
      )}

      {/* Tab 5 — Others */}
      {activeTab === 5 && (
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }}>
          <Field label="Notes" span={2}>
            <textarea className="input" rows={4} value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} placeholder="Additional context, source clause, special conditions…" />
          </Field>
          <Field label="Created By">
            <input className="input" disabled value={prefill?.created_by || ""} placeholder="Auto-set on save" style={{ background: "var(--bg-muted, #f7f7f7)", color: "var(--ink-muted)" }} />
          </Field>
          <Field label="Created At">
            <input className="input" disabled value={prefill?.created_at ? prefill.created_at.slice(0, 16).replace("T", " ") : ""} placeholder="Auto-set on save" style={{ background: "var(--bg-muted, #f7f7f7)", color: "var(--ink-muted)" }} />
          </Field>
          <Field label="Modified By">
            <input className="input" disabled value={prefill?.modified_by || ""} placeholder="Auto-set on save" style={{ background: "var(--bg-muted, #f7f7f7)", color: "var(--ink-muted)" }} />
          </Field>
          <Field label="Modified At">
            <input className="input" disabled value={prefill?.modified_at ? prefill.modified_at.slice(0, 16).replace("T", " ") : ""} placeholder="Auto-set on save" style={{ background: "var(--bg-muted, #f7f7f7)", color: "var(--ink-muted)" }} />
          </Field>
        </div>
      )}

      {/* Violations / error */}
      {violations.length > 0 && (
        <div style={{ marginTop: 16, padding: 12, background: "var(--amber-soft)", border: "1px solid #f0d8a8", borderRadius: 6 }}>
          {violations.map((v, i) => (
            <div key={i} className="row gap-8 tiny" style={{ marginBottom: i < violations.length - 1 ? 6 : 0 }}>
              <Icon name="shield" size={14} stroke="var(--amber)" />
              <b style={{ color: "var(--amber)" }}>{v.code}</b>
              <span>{v.message}</span>
            </div>
          ))}
        </div>
      )}
      {saveError && (
        <div style={{ marginTop: 12, padding: 12, background: "var(--red-soft, #fff5f5)", borderRadius: 6, color: "var(--red)", fontSize: 13 }}>
          {saveError}
        </div>
      )}
    </SlideOver>
  );
}

// ============== IMPORT WIZARD ==============
function downloadRateTemplate() {
  const headers = [
    // Core — Required
    "terminal_code", "cost_type_code", "cost_category", "sap_material_code", "charge_unit_code",
    // Container dimensions
    "containertype_code", "container_size", "container_fullness", "shipment_type_code",
    // Rate & currency
    "rate_local", "primary_currency", "effective_date", "expiry_date",
    // Slab / tier
    "slab_unit_code", "slab_type_code", "slab_min", "slab_max",
    // Corridor / logistics
    "transport_type", "corridor_from", "corridor_to", "route",
    // Vendor & contract
    "sap_vendor_code", "contract_ref", "contract_owner", "payment_terms",
    "brand_company_code", "payable_by",
    // Flags
    "is_invoicerate", "stcy_flag", "calculator", "status",
    // Notes
    "notes",
  ];
  const example = [
    "CNSHA", "DISCHARGE", "CYB", "MAT-001", "PER_TEU",
    "DRY", "20", "Empty", "IMPORT",
    "650.00", "CNY", "2025-07-01", "2026-06-30",
    "", "", "", "",
    "", "", "", "",
    "VEN-001", "CTR-APA-2025-001", "John Smith", "Net 30",
    "", "Maersk",
    "false", "false", "true", "Active",
    "",
  ];
  const hints = [
    // Core
    "# [Required] See Metadata > Terminals for valid codes",
    "# [Required] See Metadata > Cost Types for valid codes (e.g. DISCHARGE)",
    "# [Optional] CYB / CYM / PE",
    "# [Optional] SAP Material Code",
    "# [Optional] PER_TEU / PER_FEU / PER_BOX / PER_DAY / PER_MOVE / PER_KWH",
    // Container
    "# [Optional] DRY / REEFER / All",
    "# [Optional] 20 / 40 / 45 / All",
    "# [Optional] Empty / Full / All",
    "# [Optional] IMPORT / EXPORT / All. See Metadata > Move Types for valid codes",
    // Rate
    "# [Required] Numeric e.g. 650.00",
    "# [Required] ISO code e.g. CNY USD SGD",
    "# [Required] YYYY-MM-DD",
    "# [Optional] YYYY-MM-DD. Leave blank for open-ended",
    // Slab
    "# [Optional] Slab Unit code",
    "# [Optional] Slab Type code",
    "# [Optional] Numeric",
    "# [Optional] Numeric. Must be >= slab_min if both filled",
    // Corridor
    "# [Optional] Free / Rail / Barge / Road / Sea",
    "# [Optional] Corridor origin",
    "# [Optional] Corridor destination",
    "# [Optional] Route description",
    // Vendor & contract
    "# [Optional] SAP Vendor Code",
    "# [Optional] Contract reference number",
    "# [Optional] Contract owner name",
    "# [Optional] e.g. Net 30",
    "# [Optional] Brand Company code. See Metadata > Brand Companies",
    "# [Optional] Maersk / Customer. Default: Maersk",
    // Flags
    "# [Optional] true / false. Default: false",
    "# [Optional] true / false. Default: false",
    "# [Optional] true / false. Default: false",
    "# [Optional] Active / Draft / Inactive. Default: Active",
    // Notes
    "# [Optional] Free text",
  ];
  const rows = [
    headers.join(","),
    hints.join(","),
    example.join(","),
  ];
  const csv = rows.join("\r\n");
  const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = "rate_import_template.csv";
  a.click();
  URL.revokeObjectURL(url);
}

const RATEFLOW_EXCEL_COLS = [
  "TerminalCode", "ChargeCode", "ChargeName", "ContainerType",
  "Size", "FullEmpty", "ShipmentType", "Rate", "Currency",
  "ValidFrom", "ValidTo", "VendorCode", "PaymentTerms",
];

const OCMS_TARGET_FIELDS = [
  { value: "",                   label: "— Ignore —" },
  // Core identifiers
  { value: "terminal_code",      label: "Terminal Code" },
  { value: "cost_type_code",     label: "Rate Type (Cost Type)" },
  { value: "cost_category",      label: "Cost Category (CYB/CYM/PE)" },
  { value: "sap_material_code",  label: "SAP Material Code" },
  { value: "charge_unit_code",   label: "Charge Unit" },
  // Container dimensions
  { value: "containertype_code", label: "Container Type" },
  { value: "container_size",     label: "Container Size" },
  { value: "container_fullness", label: "Container Fullness" },
  { value: "shipment_type_code", label: "Shipment Type" },
  // Rate & currency
  { value: "rate_local",         label: "Rate (Local)" },
  { value: "primary_currency",   label: "Primary Currency (ISO code e.g. CNY)" },
  // Dates
  { value: "effective_date",     label: "Effective Date" },
  { value: "expiry_date",        label: "Expiry Date" },
  // Slab / tier
  { value: "slab_unit_code",     label: "Slab Unit" },
  { value: "slab_type_code",     label: "Slab Type" },
  { value: "slab_min",           label: "Slab Min" },
  { value: "slab_max",           label: "Slab Max" },
  // Vendor & contract
  { value: "sap_vendor_code",    label: "Vendor Code" },
  { value: "contract_ref",       label: "Contract Ref" },
  { value: "contract_owner",     label: "Contract Owner" },
  { value: "payment_terms",      label: "Payment Terms" },
  { value: "brand_company_code", label: "Brand Company Code" },
  { value: "payable_by",         label: "Payable By (Maersk/Customer)" },
  // Logistics
  { value: "transport_type",     label: "Transport Type" },
  { value: "route",              label: "Route" },
  { value: "route_rate",         label: "Route Rate" },
  { value: "corridor_from",      label: "Corridor From" },
  { value: "corridor_to",        label: "Corridor To" },
  // Flags & misc
  { value: "is_invoicerate",     label: "Is Invoice Rate (true/false)" },
  { value: "stcy_flag",          label: "STCY Flag (true/false)" },
  { value: "calculator",         label: "Calculator (true/false)" },
  { value: "status",             label: "Status (Active/Draft/Inactive)" },
  { value: "notes",              label: "Notes" },
];

const DEFAULT_RF_MAPPING = {
  TerminalCode:  "terminal_code",
  ChargeCode:    "",
  ChargeName:    "cost_type_code",
  ContainerType: "containertype_code",
  Size:          "container_size",
  FullEmpty:     "container_fullness",
  ShipmentType:  "shipment_type_code",
  Rate:          "rate_local",
  Currency:      "primary_currency",
  ValidFrom:     "effective_date",
  ValidTo:       "expiry_date",
  VendorCode:    "sap_vendor_code",
  PaymentTerms:  "payment_terms",
};

function ImportWizard({ onClose, onDone }) {
  const [step, setStep] = useState(0);
  const [source, setSource] = useState(null); // 'template' | 'dify' | 'rateflow'
  const [selectedFile, setSelectedFile] = useState(null);
  const [excelCols, setExcelCols] = useState([]);
  const [fieldMapping, setFieldMapping] = useState({});
  const [defaultValues, setDefaultValues] = useState({}); // { ocmsField → defaultValue string }
  const [dbMapping, setDbMapping] = useState({}); // saved mappings from DB keyed by excel_col
  const [excelRows, setExcelRows] = useState([]); // raw data rows from uploaded Excel file
  const [parseError, setParseError] = useState(null);
  const [saving, setSaving] = useState(false);
  const [saveMsg, setSaveMsg] = useState(null);
  const [lookups, setLookups] = useState(null);       // metadata lookup maps
  const [importState, setImportState] = useState(null); // { loading, ok, failed, errors }
  const [warnAction, setWarnAction]   = useState("skip");    // "skip" | "draft"
  const [errorAction, setErrorAction] = useState("export");  // "skip" | "export"
  const [valueMapping, setValueMapping] = useState({});      // { ocmsField: { rawValue: targetCode } }
  const fileInputRef = useRef(null);

  const acceptMap = { template: ".xlsx,.csv", rateflow: ".xlsx", dify: ".pdf" };

  // Load metadata lookups when source = rateflow or template
  useEffect(() => {
    if (source !== "rateflow" && source !== "template") return;
    Promise.all([
      apiFetch("/api/metadata/terminals/scoped"),
      apiFetchMeta("cost-types"),
      apiFetchMeta("charge-units"),
      apiFetchMeta("equipment-types"),
      apiFetchMeta("move-types"),
      apiFetchMeta("currencies"),
      apiFetchMeta("materials"),
      apiFetchMeta("slab-units"),
      apiFetchMeta("slab-types"),
      apiFetchMeta("brand-companies").catch(() => []),
    ]).then(([terms, cts, cus, eqs, mts, ccys, mats, slabU, slabT, bcs]) => {
      const byCode = arr => Object.fromEntries((Array.isArray(arr) ? arr : []).map(r => [r.code, r]));
      const byName = arr => Object.fromEntries((Array.isArray(arr) ? arr : []).map(r => [r.name, r]));
      setLookups({
        terminals:      byCode(terms),
        costTypes:      byCode(cts),
        chargeUnits:    byCode(cus),
        equipmentTypes: byCode(eqs),
        moveTypes:      byCode(mts),
        currencies:     Object.fromEntries((Array.isArray(ccys) ? ccys : []).map(r => [r.iso_code, r])),
        materials:      byCode(mats),
        slabUnits:      { ...byCode(slabU), ...byName(slabU) },
        slabTypes:      { ...byCode(slabT), ...byName(slabT) },
        brandCompanies: byCode(bcs),
      });
    }).catch(() => {});
  }, [source]);

  // Build API body from enriched row (codes → IDs)
  function buildRateBody(mapped) {
    const terminal = lookups?.terminals?.[mapped.terminal_code];
    const costType = lookups?.costTypes?.[mapped.cost_type_code];
    if (!terminal?.id || !costType?.id) return null;
    const parseNum = v => parseFloat(String(v ?? "").replace(/,/g, ""));
    const rateVal = parseNum(mapped.rate_local);
    if (isNaN(rateVal)) return null;
    const parseBool = v => ["true","1","yes"].includes(String(v || "").toLowerCase().trim()) ? true : false;
    return {
      terminal_id:        terminal.id,
      cost_type_id:       costType.id,
      cost_category:      mapped.cost_category || null,
      sap_material_code:  mapped.sap_material_code || null,
      charge_unit_id:     lookups.chargeUnits?.[mapped.charge_unit_code]?.id || null,
      containertype:      lookups.equipmentTypes?.[mapped.containertype_code]?.id || null,
      container_size:     mapped.container_size || null,
      container_fullness: mapped.container_fullness || null,
      shipment_type:      lookups.moveTypes?.[mapped.shipment_type_code]?.id || null,
      rate_local:         rateVal,
      primary_currency:   lookups.currencies?.[mapped.primary_currency]?.id || null,
      effective_date:     mapped.effective_date,
      expiry_date:        mapped.expiry_date || null,
      slab_unit:          lookups.slabUnits?.[mapped.slab_unit_code]?.id || null,
      slab_type:          lookups.slabTypes?.[mapped.slab_type_code]?.id || null,
      slab_min:           mapped.slab_min !== "" && mapped.slab_min != null ? parseNum(mapped.slab_min) : null,
      slab_max:           mapped.slab_max !== "" && mapped.slab_max != null ? parseNum(mapped.slab_max) : null,
      sap_vendor_code:    mapped.sap_vendor_code || null,
      contract_ref:       mapped.contract_ref || null,
      contract_owner:     mapped.contract_owner || null,
      payment_terms:      mapped.payment_terms || null,
      brand_company_id:   lookups?.brandCompanies?.[mapped.brand_company_code]?.id || null,
      payable_by:         mapped.payable_by || "Maersk",
      transport_type:     mapped.transport_type || null,
      route:              mapped.route || null,
      corridor_from:      mapped.corridor_from || null,
      corridor_to:        mapped.corridor_to || null,
      is_invoicerate:     mapped.is_invoicerate !== "" && mapped.is_invoicerate != null ? parseBool(mapped.is_invoicerate) : null,
      stcy_flag:          mapped.stcy_flag !== "" && mapped.stcy_flag != null ? parseBool(mapped.stcy_flag) : null,
      calculator:         mapped.calculator !== "" && mapped.calculator != null ? parseBool(mapped.calculator) : null,
      notes:              mapped.notes || null,
      source:             source === "rateflow" ? "Rateflow" : source === "dify" ? "DIFY" : "Manual Upload",
      status:             mapped.status || "Active",
      skip_validation:    true,
    };
  }

  async function doImport() {
    if (!lookups) return;
    const total = excelRows.length;
    setImportState({ loading: true, ok: 0, failed: 0, errors: [], done: 0, total });
    let ok = 0, failed = 0, errors = [];
    const errorRowsForExport = [];

    // Pre-flight: build all valid bodies (no network)
    const readyBodies = [];
    for (const rawRow of excelRows) {
      const mapped = {};
      for (const [ocmsField, excelCol] of Object.entries(fieldMapping)) {
        const fromCol = excelCol && rawRow[excelCol] !== undefined ? rawRow[excelCol] : "";
        mapped[ocmsField] = fromCol || (defaultValues[ocmsField] || "");
      }
      // Derive cost_type from material when missing
      if (!mapped.cost_type_code && mapped.sap_material_code && lookups.materials) {
        const mat = lookups.materials[mapped.sap_material_code];
        if (mat?.cost_type_name) {
          const n = mat.cost_type_name.toLowerCase().trim();
          const ct = Object.values(lookups.costTypes || {}).find(c =>
            c.name.toLowerCase() === n || c.code.toLowerCase() === n ||
            c.name.toLowerCase().includes(n) || n.includes(c.name.toLowerCase())
          );
          if (ct) mapped.cost_type_code = ct.code;
        }
      }
      // Apply user value mappings from Validate step ("" = leave blank)
      for (const [fk, mapping] of Object.entries(valueMapping || {})) {
        if (mapped[fk] && mapping[mapped[fk]] !== undefined) mapped[fk] = mapping[mapped[fk]];
      }
      // Normalize FK casing to canonical lookup codes (case-insensitive match)
      const ciResolve = (lk, v) => {
        if (!v || !lk) return v;
        if (lk[v]) return v;
        const low = v.toLowerCase();
        return Object.keys(lk).find(k => k.toLowerCase() === low) || v;
      };
      if (mapped.terminal_code)      mapped.terminal_code      = ciResolve(lookups.terminals,      mapped.terminal_code);
      if (mapped.cost_type_code)     mapped.cost_type_code     = ciResolve(lookups.costTypes,      mapped.cost_type_code);
      if (mapped.primary_currency)   mapped.primary_currency   = ciResolve(lookups.currencies,     mapped.primary_currency);
      if (mapped.charge_unit_code)   mapped.charge_unit_code   = ciResolve(lookups.chargeUnits,    mapped.charge_unit_code);
      if (mapped.containertype_code) mapped.containertype_code = ciResolve(lookups.equipmentTypes, mapped.containertype_code);
      if (mapped.shipment_type_code) mapped.shipment_type_code = ciResolve(lookups.moveTypes,      mapped.shipment_type_code);
      if (mapped.slab_unit_code)     mapped.slab_unit_code     = ciResolve(lookups.slabUnits,      mapped.slab_unit_code);
      if (mapped.slab_type_code)     mapped.slab_type_code     = ciResolve(lookups.slabTypes,      mapped.slab_type_code);

      // Classify row: error / warn / ok
      const missingRequired = !mapped.terminal_code || !mapped.cost_type_code || (mapped.rate_local === "" || mapped.rate_local === null || mapped.rate_local === undefined) || !mapped.effective_date;
      const unknownRequired = !missingRequired && lookups && (
        !lookups.terminals[mapped.terminal_code] || !lookups.costTypes[mapped.cost_type_code]
      );
      const isError = missingRequired || unknownRequired;
      const isWarn = !isError && lookups && (
        (mapped.charge_unit_code    && !lookups.chargeUnits[mapped.charge_unit_code])    ||
        (mapped.containertype_code  && !lookups.equipmentTypes[mapped.containertype_code]) ||
        (mapped.shipment_type_code  && !lookups.moveTypes[mapped.shipment_type_code])    ||
        (mapped.primary_currency    && !lookups.currencies[mapped.primary_currency])
      );

      // Apply user-chosen action
      const action = isError ? errorAction : isWarn ? warnAction : "import";
      if (action === "skip") {
        failed++;
        errors.push(`Row skipped (${isError ? "error" : "warning"}): ${mapped.terminal_code||"—"} / ${mapped.cost_type_code||"—"}`);
        continue;
      }
      if (action === "export") {
        // Collect for CSV export after import completes
        errorRowsForExport.push({ ...mapped, _issues: isError ? "Error" : "Warning" });
        failed++;
        continue;
      }

      const body = buildRateBody(mapped);
      if (!body) { failed++; errors.push(`Row skipped: terminal or cost type not found in metadata`); continue; }
      if (action === "draft") body.status = "Draft";
      readyBodies.push(body);
    }

    // Send in chunks of 50 via batch API
    const CHUNK = 50;
    let done = total - readyBodies.length; // pre-failed count as done
    setImportState({ loading: true, ok, failed, errors, done, total });

    for (let i = 0; i < readyBodies.length; i += CHUNK) {
      const chunk = readyBodies.slice(i, i + CHUNK);
      try {
        const res = await apiFetch("/api/rates/batch", {
          method: "POST",
          body: JSON.stringify({ rows: chunk }),
        });
        ok += res.ok || 0;
        failed += res.failed || 0;
        if (res.errors) errors.push(...res.errors);
      } catch (e) {
        failed += chunk.length;
        errors.push(e.message || String(e));
      }
      done += chunk.length;
      setImportState({ loading: true, ok, failed, errors, done, total });
    }

    setImportState({ loading: false, ok, failed, errors, done: total, total });
    if (ok > 0) onDone(`Imported ${ok} rates`);

    // Auto-download error rows as CSV if user chose "export"
    if (errorRowsForExport.length > 0) {
      const allKeys = [...new Set(errorRowsForExport.flatMap(r => Object.keys(r)))];
      const csvEscape = v => {
        const s = v == null ? "" : String(v);
        return s.includes(",") || s.includes('"') || s.includes("\n") ? `"${s.replace(/"/g, '""')}"` : s;
      };
      const csvLines = [
        allKeys.map(csvEscape).join(","),
        ...errorRowsForExport.map(r => allKeys.map(k => csvEscape(r[k])).join(",")),
      ];
      const blob = new Blob([csvLines.join("\n")], { type: "text/csv;charset=utf-8;" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = `import_errors_${new Date().toISOString().slice(0, 10)}.csv`;
      a.click();
      URL.revokeObjectURL(url);
    }
  }

  // Load saved mappings from DB when source = rateflow is selected
  // DB stores { excel_col → ocms_field }; we invert to { ocmsField → excelCol }
  useEffect(() => {
    if (source !== "rateflow") return;
    apiFetch("/api/field-mappings?source_type=rateflow")
      .then(rows => {
        if (!Array.isArray(rows)) return;
        const inv = {};
        rows.forEach(r => { if (r.ocms_field) inv[r.ocms_field] = r.excel_col; });
        setDbMapping(inv);
        // If file already parsed, rebuild mapping with DB values taking priority
        if (excelCols.length > 0) {
          const merged = {};
          OCMS_TARGET_FIELDS.filter(f => f.value).forEach(f => {
            merged[f.value] = autoSuggestExcel(f.value, excelCols, inv);
          });
          setFieldMapping(merged);
        }
      })
      .catch(() => {});
  }, [source]);

  // Keywords per OCMS field — used to auto-match Excel column headers
  const FIELD_KEYWORDS = {
    "terminal_code":       ["terminalcode","terminal"],
    "cost_type_code":      ["chargename","ratetype","charge"],
    "sap_material_code":   ["chargecode","materialcode","material","sapmaterial"],
    "charge_unit_code":    ["chargeunit","uom","unit"],
    "cost_category":       ["costcategory","category"],
    "containertype_code":  ["containertype","ctype","equip"],
    "container_size":      ["containersize","contsize","size"],
    "container_fullness":  ["fullempty","fullness","containerisfull"],
    "shipment_type_code":  ["shipmenttype","movetype","shipment"],
    "route_rate":          ["routerate"],
    "route":               ["route"],
    "corridor_from":       ["corridorfrom"],
    "corridor_to":         ["corridorto"],
    "transport_type":      ["transporttype","transport"],
    "rate_local":          ["rate","localrate"],
    "primary_currency":    ["currency","ccy","iso"],
    "effective_date":      ["validfrom","ratestartdate","effectivedate"],
    "expiry_date":         ["validto","rateenddate","expirydate"],
    "sap_vendor_code":     ["vendorcode","sapvendor","vendor"],
    "payment_terms":       ["paymentterm","payment"],
    "contract_ref":        ["contractref","contractno"],
    "contract_owner":      ["contractowner","owner"],
    "slab_unit_code":      ["slabunit"],
    "slab_type_code":      ["slabtype"],
    "slab_min":            ["slabmin"],
    "slab_max":            ["slabmax"],
    "is_invoicerate":      ["invoicerate","isinvoice"],
    "stcy_flag":           ["stcy"],
    "calculator":          ["calculator"],
    "status":              ["status"],
    "notes":               ["remark","note","comment"],
  };

  // Priority: DB saved (if col exists in file) → keyword match → ""
  function autoSuggestExcel(ocmsField, availableCols, savedInvMap) {
    const inv = savedInvMap !== undefined ? savedInvMap : dbMapping;
    const saved = inv[ocmsField];
    if (saved !== undefined && availableCols.includes(saved)) return saved;
    const keywords = FIELD_KEYWORDS[ocmsField] || [];
    for (const col of availableCols) {
      const n = col.toLowerCase().replace(/[^a-z0-9]/g, "");
      if (keywords.some(k => n.includes(k))) return col;
    }
    return "";
  }

  async function saveMapping() {
    // fieldMapping = { ocmsField → excelCol }; DB expects [{ excel_col, ocms_field }]
    const mappings = Object.entries(fieldMapping).map(([ocms_field, excel_col]) => ({ excel_col: excel_col || "", ocms_field }));
    setSaving(true); setSaveMsg(null);
    try {
      await apiFetch("/api/field-mappings", {
        method: "POST",
        body: JSON.stringify({ source_type: "rateflow", mappings }),
      });
      // Rebuild dbMapping in inverted format { ocmsField → excelCol }
      const newInv = {};
      mappings.forEach(m => { if (m.ocms_field) newInv[m.ocms_field] = m.excel_col; });
      setDbMapping(newInv);
      setSaveMsg("Mapping saved");
      setTimeout(() => setSaveMsg(null), 2500);
    } catch (e) {
      setSaveMsg("Save failed: " + e.message);
    }
    setSaving(false);
  }

  function parseExcelHeaders(file, savedMap) {
    setParseError(null);
    const reader = new FileReader();
    reader.onload = (e) => {
      try {
        const wb = XLSX.read(e.target.result, { type: "array" });
        const ws = wb.Sheets[wb.SheetNames[0]];
        const range = XLSX.utils.decode_range(ws["!ref"] || "A1:A1");
        const cols = [];
        for (let c = range.s.c; c <= range.e.c; c++) {
          const cell = ws[XLSX.utils.encode_cell({ r: range.s.r, c })];
          if (cell && cell.v != null) cols.push(String(cell.v).trim());
        }
        setExcelCols(cols);
        // Read all data rows (skip header row 0)
        const allRows = XLSX.utils.sheet_to_json(ws, { header: 1, raw: false, defval: "" });
        const dataRows = allRows.slice(1).map(row =>
          Object.fromEntries(cols.map((col, ci) => [col, row[ci] != null ? String(row[ci]).trim() : ""]))
        );
        setExcelRows(dataRows);
        // Build mapping { ocmsField → excelCol }: DB saved takes priority, then keyword match
        const inv = savedMap || dbMapping;
        const initMap = {};
        OCMS_TARGET_FIELDS.filter(f => f.value).forEach(f => {
          initMap[f.value] = autoSuggestExcel(f.value, cols, inv);
        });
        setFieldMapping(initMap);
      } catch (err) {
        setParseError("Failed to read Excel headers: " + err.message);
      }
    };
    reader.readAsArrayBuffer(file);
  }

  function parseTemplateFile(file) {
    setParseError(null);
    const reader = new FileReader();
    reader.onload = (e) => {
      try {
        const wb = XLSX.read(e.target.result, { type: "array" });
        const ws = wb.Sheets[wb.SheetNames[0]];
        const allRows = XLSX.utils.sheet_to_json(ws, { header: 1, raw: false, defval: "" });
        if (allRows.length === 0) { setParseError("No data found in file."); return; }
        const rawHeaders = allRows[0].map(h => String(h).trim());
        // Normalise column name aliases (old template used "shipment_type", current uses "shipment_type_code")
        const COL_ALIASES = { shipment_type: "shipment_type_code" };
        const headers = rawHeaders.map(h => COL_ALIASES[h] || h);
        // Row 1 is the hints row (starts with "#") — skip it; data starts at row 2
        const dataStartRow = allRows.length > 1 && String(allRows[1][0] || "").startsWith("#") ? 2 : 1;
        // Normalize date values: M/D/YYYY → YYYY-MM-DD
        const normDate = v => {
          if (!v) return "";
          if (/^\d{4}-\d{2}-\d{2}$/.test(v)) return v;
          const m = v.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
          if (m) return `${m[3]}-${m[1].padStart(2,"0")}-${m[2].padStart(2,"0")}`;
          return v;
        };
        const DATE_COLS = new Set(["effective_date", "expiry_date"]);
        const dataRows = allRows.slice(dataStartRow).filter(row => row.some(c => c !== "")).map(row =>
          Object.fromEntries(headers.map((h, i) => {
            let v = row[i] != null ? String(row[i]).trim() : "";
            if (DATE_COLS.has(h)) v = normDate(v);
            return [h, v];
          }))
        );
        setExcelRows(dataRows);
        // Identity mapping: template column names ARE the ocms field names
        const identityMap = {};
        OCMS_TARGET_FIELDS.filter(f => f.value).forEach(f => {
          if (headers.includes(f.value)) identityMap[f.value] = f.value;
        });
        setFieldMapping(identityMap);
        setExcelCols(headers);
      } catch (err) {
        setParseError("Failed to read file: " + err.message);
      }
    };
    reader.readAsArrayBuffer(file);
  }

  function handleFileChange(e) {
    const f = e.target.files && e.target.files[0];
    if (f) {
      setSelectedFile(f);
      if (source === "rateflow") parseExcelHeaders(f, dbMapping);
      else if (source === "template") parseTemplateFile(f);
    }
    e.target.value = "";
  }

  function handleDrop(e) {
    e.preventDefault();
    e.currentTarget.classList.remove("hover");
    const f = e.dataTransfer.files && e.dataTransfer.files[0];
    if (f) {
      setSelectedFile(f);
      if (source === "rateflow") parseExcelHeaders(f, dbMapping);
      else if (source === "template") parseTemplateFile(f);
    }
  }

  function fmtSize(bytes) {
    if (bytes < 1024) return bytes + " B";
    if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(0) + " KB";
    return (bytes / 1024 / 1024).toFixed(1) + " MB";
  }

  // Pre-compute validation counts for Confirm summary
  const previewCounts = useMemo(() => {
    if (!lookups || excelRows.length === 0) return null;
    let ok = 0, warn = 0, error = 0;
    const applyVM = (fk, v) => v ? ((valueMapping[fk] || {})[v] ?? v) : v;
    for (const rawRow of excelRows) {
      const mapped = {};
      for (const [ocmsField, excelCol] of Object.entries(fieldMapping)) {
        const fromCol = excelCol && rawRow[excelCol] !== undefined ? rawRow[excelCol] : "";
        mapped[ocmsField] = fromCol || (defaultValues[ocmsField] || "");
      }
      // Enrich cost_type from material
      if (!mapped.cost_type_code && mapped.sap_material_code && lookups.materials) {
        const mat = lookups.materials[mapped.sap_material_code];
        if (mat?.cost_type_name) {
          const n = mat.cost_type_name.toLowerCase().trim();
          const ct = Object.values(lookups.costTypes || {}).find(c =>
            c.name.toLowerCase() === n || c.code.toLowerCase() === n ||
            c.name.toLowerCase().includes(n) || n.includes(c.name.toLowerCase())
          );
          if (ct) mapped.cost_type_code = ct.code;
        }
      }
      // Apply user value mappings
      for (const f of IMPORT_VALIDATE_FIELDS) {
        if (f.hasFk && mapped[f.key]) mapped[f.key] = applyVM(f.key, mapped[f.key]);
      }
      const missingRequired = !mapped.terminal_code || !mapped.cost_type_code || (mapped.rate_local === "" || mapped.rate_local === null || mapped.rate_local === undefined) || !mapped.effective_date;
      const unknownRequired = !missingRequired && (
        !lookups.terminals[mapped.terminal_code] || !lookups.costTypes[mapped.cost_type_code]
      );
      const isError = missingRequired || unknownRequired;
      const isWarn = !isError && (
        (mapped.charge_unit_code    && !lookups.chargeUnits[mapped.charge_unit_code])    ||
        (mapped.containertype_code  && !lookups.equipmentTypes[mapped.containertype_code]) ||
        (mapped.shipment_type_code  && !lookups.moveTypes[mapped.shipment_type_code])    ||
        (mapped.primary_currency    && !lookups.currencies[mapped.primary_currency])
      );
      if (isError) error++; else if (isWarn) warn++; else ok++;
    }
    return { ok, warn, error };
  }, [excelRows, fieldMapping, defaultValues, lookups, valueMapping]);

  // Rateflow gets an extra "Field Mapping" step between Upload and Preview
  const isRateflow = source === "rateflow";
  const steps = isRateflow
    ? ["Upload", "Field Mapping", "Validate & Preview", "Confirm"]
    : ["Upload", "Validate & Preview", "Confirm"];
  const maxStep = steps.length - 1;
  const isConfirmStep = step === maxStep;
  // logical step aliases (independent of rateflow or not)
  const isFieldMappingStep = isRateflow && step === 1;
  const isPreviewStep = isRateflow ? step === 2 : step === 1;

  // Columns to display in mapping — use parsed cols, fall back to default list if parsing hasn't run
  const mappingCols = excelCols.length > 0 ? excelCols : RATEFLOW_EXCEL_COLS;

  const confirmCounts = { dify: { ok: 11, total: 12 }, template: { ok: 47, total: 47 }, rateflow: { ok: 63, total: 65 } };
  const cc = confirmCounts[source] || { ok: 0, total: 0 };

  // Count mapped OCMS fields (fieldMapping = { ocmsField → excelCol })
  const mappedCount = Object.values(fieldMapping).filter(Boolean).length;
  // Number of distinct excel cols assigned (for upload badge)
  const assignedExcelCount = new Set(Object.values(fieldMapping).filter(Boolean)).size;
  const ocmsFieldCount = OCMS_TARGET_FIELDS.filter(f => f.value).length;

  return (
    <SlideOver
      open={true}
      onClose={onClose}
      wide
      title="Import rates"
      sub="3-step wizard · template, AI-extracted contract or Rateflow raw data"
      foot={
        <>
          <button className="btn ghost" onClick={onClose}>Cancel</button>
          {step > 0 && <button className="btn" onClick={() => setStep(s => s - 1)}>Back</button>}
          <div className="grow" />
          {!isConfirmStep && (
            <button
              className="btn primary"
              disabled={step === 0 && (!source || !selectedFile)}
              onClick={() => setStep(s => s + 1)}
            >
              Continue <Icon name="arrowRight" size={12} />
            </button>
          )}
          {isConfirmStep && !importState?.loading && !importState?.ok && (
            <button className="btn primary" onClick={doImport} disabled={!lookups && (source === "rateflow" || source === "template")}>
              Confirm import
            </button>
          )}
          {isConfirmStep && importState && !importState.loading && (
            <button className="btn primary" onClick={onClose}>Close</button>
          )}
        </>
      }
    >
      <div style={{ marginBottom: 20 }}><Stepper steps={steps} current={step} /></div>

      {step === 0 && (
        <div>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 12 }}>
            <div onClick={() => setSource("template")}
              className="card"
              style={{ padding: 18, cursor: "pointer", borderColor: source === "template" ? "var(--teal)" : "var(--line)", borderWidth: source === "template" ? 2 : 1 }}>
              <div className="row gap-8">
                <Icon name="table" size={20} stroke="var(--ink)" />
                <b>Excel Template</b>
              </div>
              <div className="tiny muted" style={{ marginTop: 8, lineHeight: 1.55 }}>
                Download the pre-filled template with metadata dropdowns. Fill in rows, upload and validate.
              </div>
              <button className="btn" style={{ marginTop: 12 }} onClick={e => { e.stopPropagation(); downloadRateTemplate(); }}><Icon name="download" size={12} /> Download template</button>
            </div>

            <div onClick={() => setSource("rateflow")}
              className="card"
              style={{ padding: 18, cursor: "pointer", borderColor: source === "rateflow" ? "var(--teal)" : "var(--line)", borderWidth: source === "rateflow" ? 2 : 1 }}>
              <div className="row gap-8">
                <Icon name="barChart" size={20} stroke="var(--blue, #2196f3)" />
                <b>Rateflow Raw Data</b>
                <Pill className="plain">Excel</Pill>
              </div>
              <div className="tiny muted" style={{ marginTop: 8, lineHeight: 1.55 }}>
                Upload Rateflow system export (.xlsx). Columns are auto-mapped and validated against OCMS metadata.
              </div>
              <div className="tiny mono muted" style={{ marginTop: 12 }}>Supports Rateflow v2 / v3 export format</div>
            </div>

            <div className="card"
              style={{ padding: 18, cursor: "not-allowed", opacity: 0.45, borderColor: "var(--line)", borderWidth: 1, position: "relative" }}>
              <div className="row gap-8">
                <Icon name="sparkle" size={20} stroke="var(--ink-mute)" />
                <b style={{ color: "var(--ink-mute)" }}>Contract PDF (AI)</b>
                <Pill className="plain">DIFY</Pill>
                <span style={{ marginLeft: "auto", fontSize: 10, fontWeight: 500, color: "var(--ink-mute)", background: "var(--line)", borderRadius: 4, padding: "2px 7px", letterSpacing: "0.04em" }}>Under Development</span>
              </div>
              <div className="tiny muted" style={{ marginTop: 8, lineHeight: 1.55 }}>
                Upload a contract PDF; AI extracts rates, you review and confirm. Async — notified when ready.
              </div>
              <div className="tiny mono muted" style={{ marginTop: 12 }}>PDF not stored · Contract Ref captured</div>
            </div>
          </div>

          {source && (
            <div style={{ marginTop: 18 }}>
              {source === "rateflow" && (
                <div className="card" style={{ marginBottom: 12, padding: "10px 14px", background: "var(--blue-soft, #e8f4ff)", borderColor: "var(--blue-line, #b3d4f5)" }}>
                  <div className="row gap-8">
                    <Icon name="info" size={14} stroke="var(--blue, #2196f3)" />
                    <span className="tiny" style={{ color: "var(--blue, #2196f3)" }}>
                      Expected columns: <span className="mono">TerminalCode, ChargeCode, ChargeName, ContainerType, Size, FullEmpty, ShipmentType, Rate, Currency, ValidFrom, ValidTo, VendorCode, PaymentTerms</span>
                    </span>
                  </div>
                </div>
              )}
              <input
                ref={fileInputRef}
                type="file"
                accept={acceptMap[source]}
                style={{ display: "none" }}
                onChange={handleFileChange}
              />
              {!selectedFile ? (
                <div
                  className="dropzone"
                  style={{ cursor: "pointer" }}
                  onClick={() => fileInputRef.current && fileInputRef.current.click()}
                  onDragOver={e => { e.preventDefault(); e.currentTarget.classList.add("hover"); }}
                  onDragLeave={e => e.currentTarget.classList.remove("hover")}
                  onDrop={handleDrop}
                >
                  <Icon name="upload" size={28} stroke="var(--ink-mute)" />
                  <div style={{ marginTop: 10, fontWeight: 500, color: "var(--ink)" }}>
                    Drop {source === "dify" ? "contract PDF" : source === "rateflow" ? "Rateflow .xlsx file" : ".xlsx / .csv file"} here
                  </div>
                  <div className="tiny" style={{ marginTop: 4 }}>
                    or <span style={{ color: "var(--teal)", cursor: "pointer", textDecoration: "underline" }}>browse</span> — max 25MB
                  </div>
                  <div className="tiny muted" style={{ marginTop: 6 }}>
                    {source === "rateflow" ? "Accepts: .xlsx" : source === "dify" ? "Accepts: .pdf" : "Accepts: .xlsx, .csv"}
                  </div>
                </div>
              ) : (
                <div className="card" style={{ padding: 12, display: "flex", alignItems: "center", gap: 10 }}>
                  <div style={{ width: 36, height: 36, borderRadius: 6, background: "var(--paper-2)", display: "grid", placeItems: "center" }}>
                    <Icon name={source === "dify" ? "pdf" : "file"} size={18} stroke="var(--ink-mute)" />
                  </div>
                  <div style={{ flex: 1 }}>
                    <div className="bold">{selectedFile.name}</div>
                    <div className="tiny muted mono">{fmtSize(selectedFile.size)}</div>
                  </div>
                  {source === "dify" && (
                    <div className="row gap-6">
                      <div style={{ width: 8, height: 8, borderRadius: 50, background: "var(--teal)", animation: "pulse 1.5s infinite" }} />
                      <span className="tiny muted">Parsing via DIFY…</span>
                    </div>
                  )}
                  {source === "rateflow" && excelCols.length > 0 && (
                    <div className="row gap-6">
                      <div style={{ width: 8, height: 8, borderRadius: 50, background: assignedExcelCount === excelCols.length ? "var(--green)" : "var(--amber)" }} />
                      <span className="tiny muted">{assignedExcelCount}/{excelCols.length} columns matched</span>
                    </div>
                  )}
                  {source === "rateflow" && excelCols.length === 0 && parseError && (
                    <div className="row gap-6">
                      <div style={{ width: 8, height: 8, borderRadius: 50, background: "var(--red, #d32f2f)" }} />
                      <span className="tiny muted" style={{ color: "var(--red, #d32f2f)" }}>Parse error</span>
                    </div>
                  )}
                  {source === "rateflow" && excelCols.length === 0 && !parseError && (
                    <div className="row gap-6">
                      <div style={{ width: 8, height: 8, borderRadius: 50, background: "var(--ink-mute)", opacity: 0.4 }} />
                      <span className="tiny muted">Reading headers…</span>
                    </div>
                  )}
                  <button className="icon-btn" onClick={() => setSelectedFile(null)}><Icon name="close" size={12} /></button>
                </div>
              )}
            </div>
          )}
        </div>
      )}

      {/* Field Mapping step — Rateflow only */}
      {isFieldMappingStep && (
        <div>
          <div style={{ marginBottom: 14 }}>
            <div className="bold" style={{ marginBottom: 4 }}>
              Map OCMS fields to Excel columns
              {excelCols.length > 0 && (
                <span className="tiny muted" style={{ marginLeft: 8, fontWeight: 400 }}>
                  {excelCols.length} columns detected from <span className="mono">{selectedFile?.name}</span>
                </span>
              )}
            </div>
            <div className="tiny muted">
              {excelCols.length > 0
                ? "Select which Excel column corresponds to each OCMS field. Each column can only be used once — already assigned columns are hidden from other dropdowns."
                : "Upload a file first to detect actual column headers from your Rateflow export."}
            </div>
          </div>
          {parseError && (
            <div className="card" style={{ marginBottom: 12, padding: "8px 12px", background: "var(--red-soft, #fdecea)", borderColor: "#f5c6c6" }}>
              <div className="row gap-8 tiny">
                <Icon name="shield" size={13} stroke="var(--red, #d32f2f)" />
                <span style={{ color: "var(--red, #d32f2f)" }}>{parseError}</span>
              </div>
            </div>
          )}
          <div className="table-wrap">
            <table className="table" style={{ fontSize: 13 }}>
              <thead>
                <tr>
                  <th style={{ width: 36 }}>#</th>
                  <th>OCMS Field</th>
                  <th>Excel Column (from file)</th>
                  <th>Default Value</th>
                  <th style={{ width: 64, textAlign: "center" }}>Status</th>
                </tr>
              </thead>
              <tbody>
                {OCMS_TARGET_FIELDS.filter(f => f.value).map((f, i) => {
                  const selectedCol = fieldMapping[f.value] || "";
                  const defVal = defaultValues[f.value] || "";
                  // Excel cols already chosen by other OCMS fields
                  const usedByOthers = new Set(
                    Object.entries(fieldMapping)
                      .filter(([k, v]) => k !== f.value && v)
                      .map(([, v]) => v)
                  );
                  const availableCols = (excelCols.length > 0 ? excelCols : [])
                    .filter(col => !usedByOthers.has(col) || col === selectedCol);
                  const isMapped = !!selectedCol;
                  const hasDefault = !isMapped && !!defVal;
                  return (
                    <tr key={f.value}>
                      <td className="tiny muted mono">{i + 1}</td>
                      <td><span style={{ color: "var(--ink)", fontWeight: 500 }}>{f.label}</span></td>
                      <td>
                        <select
                          className="input"
                          style={{ fontSize: 12, padding: "4px 8px" }}
                          value={selectedCol}
                          onChange={e => setFieldMapping(m => ({ ...m, [f.value]: e.target.value }))}
                          disabled={excelCols.length === 0}
                        >
                          <option value="">— Ignore —</option>
                          {availableCols.map(col => (
                            <option key={col} value={col}>{col}</option>
                          ))}
                        </select>
                      </td>
                      <td>
                        {isMapped
                          ? <span className="tiny muted">—</span>
                          : <input
                              type="text"
                              className="input"
                              style={{ fontSize: 12, padding: "4px 8px", width: "100%" }}
                              placeholder="Enter default…"
                              value={defVal}
                              onChange={e => setDefaultValues(d => ({ ...d, [f.value]: e.target.value }))}
                            />
                        }
                      </td>
                      <td style={{ textAlign: "center" }}>
                        {isMapped
                          ? <span style={{ color: "var(--green)", fontSize: 11 }}>✓ Col</span>
                          : hasDefault
                            ? <span style={{ color: "var(--blue,#1565c0)", fontSize: 11 }}>✓ Def</span>
                            : <span className="muted tiny">—</span>}
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
          <div className="row gap-8 tiny muted" style={{ marginTop: 10 }}>
            <Icon name="info" size={13} />
            <span><b>{mappedCount}</b> of {ocmsFieldCount} OCMS fields mapped · <b>{excelCols.length - assignedExcelCount}</b> excel columns ignored</span>
            <div className="grow" />
            {saveMsg && <span style={{ color: saveMsg.startsWith("Save failed") ? "var(--red,#d32f2f)" : "var(--green)", fontSize: 11 }}>{saveMsg}</span>}
            <button className="btn" style={{ fontSize: 11, padding: "3px 10px" }} disabled={saving} onClick={saveMapping}>
              {saving ? "Saving…" : "Save mapping"}
            </button>
            <button className="btn" style={{ fontSize: 11, padding: "3px 10px" }} onClick={() => {
              const reset = {};
              OCMS_TARGET_FIELDS.filter(f => f.value).forEach(f => {
                reset[f.value] = autoSuggestExcel(f.value, excelCols, {});
              });
              setFieldMapping(reset);
            }}>
              Reset to default
            </button>
          </div>
        </div>
      )}

      {isPreviewStep && (
        <ImportPreview
          source={source}
          excelRows={excelRows}
          fieldMapping={fieldMapping}
          defaultValues={defaultValues}
          selectedFile={selectedFile}
          excelCols={excelCols}
          assignedExcelCount={assignedExcelCount}
          lookups={lookups}
          warnAction={warnAction}
          setWarnAction={setWarnAction}
          errorAction={errorAction}
          setErrorAction={setErrorAction}
          valueMapping={valueMapping}
          setValueMapping={setValueMapping}
        />
      )}

      {isConfirmStep && (
        <div style={{ textAlign: "center", padding: "40px 20px" }}>
          {importState?.loading ? (
            <>
              <div style={{ marginBottom: 16 }}>
                <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6 }}>
                  <span className="tiny muted">Importing…</span>
                  <span className="tiny mono">{importState.done} / {importState.total}</span>
                </div>
                <div style={{ height: 8, borderRadius: 4, background: "var(--line)", overflow: "hidden" }}>
                  <div style={{
                    height: "100%", borderRadius: 4,
                    background: "var(--teal, #009eb4)",
                    width: `${importState.total > 0 ? Math.round(importState.done / importState.total * 100) : 0}%`,
                    transition: "width 0.25s ease",
                  }} />
                </div>
                <div className="tiny muted" style={{ marginTop: 6 }}>
                  {importState.ok > 0 && <span style={{ color: "var(--green)" }}>{importState.ok} saved  </span>}
                  {importState.failed > 0 && <span style={{ color: "var(--red)" }}>{importState.failed} skipped</span>}
                </div>
              </div>
              <div className="tiny muted">Do not close this window.</div>
            </>
          ) : importState ? (
            <>
              <div style={{ width: 56, height: 56, borderRadius: "50%", background: importState.failed === 0 ? "var(--green-soft)" : "var(--amber-soft,#fff8e1)", color: importState.failed === 0 ? "var(--green)" : "var(--amber)", display: "grid", placeItems: "center", margin: "0 auto 16px" }}>
                <Icon name={importState.failed === 0 ? "check" : "alertTriangle"} size={28} />
              </div>
              <div className="serif" style={{ fontSize: 20, fontWeight: 500, marginBottom: 6 }}>Import complete</div>
              <div style={{ display: "inline-block", marginTop: 12, padding: 14, background: "var(--paper)", borderRadius: 6, textAlign: "left" }}>
                <div className="tiny mono muted">RESULT</div>
                <div style={{ marginTop: 4 }}>
                  <span style={{ color: "var(--green)" }}><b>{importState.ok}</b> imported</span>
                  {importState.failed > 0 && <span style={{ color: "var(--red)", marginLeft: 12 }}><b>{importState.failed}</b> failed</span>}
                </div>
                {importState.errors.length > 0 && (
                  <div style={{ marginTop: 8, maxHeight: 120, overflowY: "auto" }}>
                    {importState.errors.slice(0, 10).map((e, i) => (
                      <div key={i} className="tiny" style={{ color: "var(--red)" }}>{e}</div>
                    ))}
                    {importState.errors.length > 10 && <div className="tiny muted">… and {importState.errors.length - 10} more</div>}
                  </div>
                )}
              </div>
            </>
          ) : (
            <>
              <div style={{ width: 56, height: 56, borderRadius: "50%", background: "var(--green-soft)", color: "var(--green)", display: "grid", placeItems: "center", margin: "0 auto 16px" }}>
                <Icon name="check" size={28} />
              </div>
              <div className="serif" style={{ fontSize: 20, fontWeight: 500, marginBottom: 6 }}>Ready to commit</div>
              <div className="muted">{excelRows.length} row{excelRows.length !== 1 ? "s" : ""} loaded from file. Click "Confirm import" to write to database.</div>
              <div style={{ display: "inline-block", marginTop: 24, padding: 14, background: "var(--paper)", borderRadius: 6, textAlign: "left", minWidth: 300 }}>
                <div className="tiny mono muted">SUMMARY</div>
                <div style={{ marginTop: 4 }}><b>{excelRows.length}</b> rows total · Source: <span className="mono">{source}</span></div>
                {previewCounts && (
                  <div style={{ marginTop: 10, display: "grid", gridTemplateColumns: "16px 1fr auto", gap: "5px 10px", alignItems: "center" }}>
                    <div style={{ width: 8, height: 8, borderRadius: "50%", background: "var(--green)" }} />
                    <span className="tiny"><b>{previewCounts.ok}</b> valid</span>
                    <span className="tiny" style={{ color: "var(--green)" }}>Import as Active</span>
                    <div style={{ width: 8, height: 8, borderRadius: "50%", background: "var(--amber,#f59e0b)" }} />
                    <span className="tiny"><b>{previewCounts.warn}</b> warnings</span>
                    <span className="tiny" style={{ color: "var(--amber,#f59e0b)" }}>
                      {warnAction === "draft" ? "Move to Draft" : warnAction === "export" ? "Export to CSV" : "Ignore"}
                    </span>
                    <div style={{ width: 8, height: 8, borderRadius: "50%", background: "var(--red,#dc2626)" }} />
                    <span className="tiny"><b>{previewCounts.error}</b> errors</span>
                    <span className="tiny" style={{ color: "var(--red,#dc2626)" }}>
                      {errorAction === "export" ? "Export to CSV" : "Ignore"}
                    </span>
                  </div>
                )}
                <div className="tiny muted" style={{ marginTop: 8 }}>Audit entry will be created on confirm.</div>
              </div>
            </>
          )}
        </div>
      )}
    </SlideOver>
  );
}

// ─── Field definitions for Validate & Preview step ───────────────────────────
const IMPORT_VALIDATE_FIELDS = [
  { key: "terminal_code",      label: "Terminal",        lookupKey: "terminals",      required: true,  hasFk: true },
  { key: "cost_type_code",     label: "Cost Type",       lookupKey: "costTypes",      required: true,  hasFk: true },
  { key: "effective_date",     label: "Effective Date",  lookupKey: null,             required: true,  hasFk: false },
  { key: "rate_local",         label: "Rate",            lookupKey: null,             required: true,  hasFk: false },
  { key: "primary_currency",   label: "Currency",        lookupKey: "currencies",     required: false, hasFk: true },
  { key: "charge_unit_code",   label: "Charge Unit",     lookupKey: "chargeUnits",    required: false, hasFk: true },
  { key: "containertype_code", label: "Container Type",  lookupKey: "equipmentTypes", required: false, hasFk: true },
  { key: "shipment_type_code", label: "Shipment Type",   lookupKey: "moveTypes",      required: false, hasFk: true },
  { key: "slab_unit_code",     label: "Slab Unit",       lookupKey: "slabUnits",      required: false, hasFk: true },
  { key: "slab_type_code",     label: "Slab Type",       lookupKey: "slabTypes",      required: false, hasFk: true },
];

function ImportPreview({ source, excelRows, fieldMapping, defaultValues, selectedFile, excelCols, assignedExcelCount, lookups, warnAction, setWarnAction, errorAction, setErrorAction, valueMapping, setValueMapping }) {

  // ── Modal state ──
  const [inspectModal, setInspectModal] = useState(null); // { fieldDef, colType } | null
  const [pendingMap, setPendingMap]     = useState({});   // rawVal → targetCode (staging inside modal)

  // ── Helpers ──
  function applyRawMapping(rawRow) {
    const result = {};
    for (const [ocmsField, excelCol] of Object.entries(fieldMapping)) {
      const fromCol = excelCol && rawRow[excelCol] !== undefined ? rawRow[excelCol] : "";
      result[ocmsField] = fromCol || (defaultValues[ocmsField] || "");
    }
    return result;
  }
  function applyValMap(fieldKey, rawVal) {
    if (!rawVal) return rawVal;
    return (valueMapping[fieldKey] || {})[rawVal] ?? rawVal;
  }
  // Case-insensitive lookup: returns canonical code from lookup, or null if not found
  function resolveCode(lookup, val) {
    if (!lookup || !val) return null;
    if (lookup[val]) return val;
    const lower = val.toLowerCase();
    const found = Object.keys(lookup).find(k => k.toLowerCase() === lower);
    return found || null;
  }
  function enrichRow(mapped) {
    const enriched = { ...mapped };
    if (!enriched.cost_type_code && enriched.sap_material_code && lookups?.materials) {
      const mat = lookups.materials[enriched.sap_material_code];
      if (mat?.cost_type_name) {
        const n = mat.cost_type_name.toLowerCase().trim();
        const ct = Object.values(lookups.costTypes || {}).find(c =>
          c.name.toLowerCase() === n || c.code.toLowerCase() === n ||
          c.name.toLowerCase().includes(n) || n.includes(c.name.toLowerCase())
        );
        if (ct) enriched.cost_type_code = ct.code;
      }
    }
    // Normalize FK field casing to canonical lookup codes
    if (lookups) {
      for (const f of IMPORT_VALIDATE_FIELDS) {
        if (f.hasFk && enriched[f.key] && lookups[f.lookupKey]) {
          const canonical = resolveCode(lookups[f.lookupKey], enriched[f.key]);
          if (canonical) enriched[f.key] = canonical;
        }
      }
    }
    return enriched;
  }
  function evalField(fieldDef, rawVal) {
    const val = applyValMap(fieldDef.key, rawVal);
    if (!fieldDef.hasFk) {
      if (fieldDef.key === "rate_local") return (val === "" || val === null || val === undefined) ? "error" : "valid";
      return !val ? "error" : "valid";
    }
    const lookup = lookups?.[fieldDef.lookupKey];
    if (!val) return fieldDef.required ? "error" : "valid";
    if (!lookups) return "loading";
    return resolveCode(lookup, val) ? "valid" : (fieldDef.required ? "error" : "warn");
  }

  // ── Computed rows & stats (hooks must be at top level) ──
  const processedRows = useMemo(() =>
    excelRows.map((rawRow, i) => ({ rowNum: i + 1, enriched: enrichRow(applyRawMapping(rawRow)) })),
    [excelRows, fieldMapping, defaultValues, lookups]
  );
  const fieldStats = useMemo(() =>
    IMPORT_VALIDATE_FIELDS.map(fieldDef => {
      let valid = 0, warn = 0, error = 0;
      for (const { enriched } of processedRows) {
        const s = evalField(fieldDef, enriched[fieldDef.key]);
        if (s === "valid") valid++; else if (s === "warn") warn++; else error++;
      }
      return { fieldDef, valid, warn, error };
    }),
    [processedRows, lookups, valueMapping]
  );
  const totalRows   = excelRows.length;
  const overallOk   = processedRows.filter(({ enriched }) => IMPORT_VALIDATE_FIELDS.every(f => evalField(f, enriched[f.key]) !== "error")).length;
  const overallWarn = processedRows.filter(({ enriched }) => IMPORT_VALIDATE_FIELDS.some(f => evalField(f, enriched[f.key]) === "warn") && IMPORT_VALIDATE_FIELDS.every(f => evalField(f, enriched[f.key]) !== "error")).length;
  const overallError = totalRows - overallOk;

  // ── Modal helpers ──
  function openModal(fieldDef, colType) {
    setInspectModal({ fieldDef, colType });
    setPendingMap({ ...(valueMapping[fieldDef.key] || {}) });
  }
  function closeModal() { setInspectModal(null); setPendingMap({}); }
  function commitMapping() {
    if (!inspectModal) return;
    setValueMapping(prev => ({ ...prev, [inspectModal.fieldDef.key]: { ...(prev[inspectModal.fieldDef.key] || {}), ...pendingMap } }));
    closeModal();
  }

  // ── DIFY placeholder (unchanged) ──
  if (source !== "rateflow" && source !== "template") {
    const sampleDify = [
      { row: 1, term: "SGSIN", ct: "CY_HANDLING", unit: "PER_TEU", rate: 168.00, ccy: "SGD", eff: "2025-07-01", conf: 0.97, status: "ok" },
      { row: 2, term: "SGSIN", ct: "CY_HANDLING", unit: "PER_FEU", rate: 252.00, ccy: "SGD", eff: "2025-07-01", conf: 0.96, status: "ok" },
      { row: 3, term: "SGSIN", ct: "STORAGE",     unit: "PER_DAY", rate: 12.50,  ccy: "SGD", eff: "2025-07-01", conf: 0.94, status: "ok", tier: "Day 1–3" },
      { row: 4, term: "SGSIN", ct: "STORAGE",     unit: "PER_DAY", rate: 25.00,  ccy: "SGD", eff: "2025-07-01", conf: 0.91, status: "ok", tier: "Day 4+" },
      { row: 5, term: "SGSIN", ct: "EXTRA_YARD",  unit: "PER_MOVE",rate: 95.00,  ccy: "SGD", eff: "2025-07-01", conf: 0.72, status: "warn" },
      { row: 6, term: "SGSIN", ct: "VGM_WEIGH",   unit: "PER_BOX", rate: 15.00,  ccy: "SGD", eff: "2025-07-01", conf: 0.55, status: "error" },
    ];
    return (
      <div className="card">
        <div className="table-wrap">
          <table className="table">
            <thead>
              <tr>
                <th style={{ width: 28 }}><input type="checkbox" defaultChecked /></th>
                <th>#</th><th>Terminal</th><th>Cost Type</th><th>Unit</th>
                <th className="num">Rate</th><th>Effective</th><th>Confidence</th><th>Result</th>
              </tr>
            </thead>
            <tbody>
              {sampleDify.map(r => (
                <tr key={r.row} style={r.status === "error" ? { background: "#fef4f4" } : r.status === "warn" ? { background: "#fdf6e7" } : null}>
                  <td><input type="checkbox" defaultChecked={r.status !== "error"} /></td>
                  <td className="mono tiny">{r.row}</td>
                  <td className="mono">{r.term}</td>
                  <td>{r.ct}{r.tier && <span className="tiny muted"> · {r.tier}</span>}</td>
                  <td className="muted">{r.unit}</td>
                  <td className="num">{fmt.num(r.rate)} <span className="tiny muted">{r.ccy}</span></td>
                  <td className="mono tiny">{r.eff}</td>
                  <td>
                    <div className="row gap-4">
                      <div style={{ width: 50, height: 4, background: "var(--paper-2)", borderRadius: 2, overflow: "hidden" }}>
                        <div style={{ width: `${r.conf * 100}%`, height: "100%", background: r.conf > 0.9 ? "var(--green)" : r.conf > 0.8 ? "var(--teal)" : "var(--amber)" }} />
                      </div>
                      <span className="tiny mono">{r.conf.toFixed(2)}</span>
                    </div>
                  </td>
                  <td>
                    {r.status === "ok"    && <span className="tiny" style={{ color: "var(--green)" }}>✓ Valid</span>}
                    {r.status === "warn"  && <span className="tiny" style={{ color: "var(--amber)" }}>⚠ Review</span>}
                    {r.status === "error" && <span className="tiny" style={{ color: "var(--red)" }}>✕ Conflict</span>}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>
    );
  }

  // ── Empty state ──
  if (excelRows.length === 0) {
    return (
      <div className="card" style={{ padding: 32, textAlign: "center", color: "var(--ink-mute)" }}>
        No data rows found. Please upload a file in the previous step.
      </div>
    );
  }

  // ── Main: field-level validation table (rateflow / template) ──
  return (
    <div>
      {/* Summary bar */}
      <div className="card" style={{ marginBottom: 12, padding: "10px 14px", background: "var(--paper-2)" }}>
        <div style={{ display: "grid", gridTemplateColumns: "repeat(5, 1fr)", gap: 10 }}>
          <div><div className="tiny muted">Source file</div><div className="tiny mono" style={{ wordBreak: "break-all" }}>{selectedFile?.name || "—"}</div></div>
          <div><div className="tiny muted">Total rows</div><div className="tiny mono">{totalRows}</div></div>
          <div><div className="tiny muted">Columns matched</div><div className="tiny mono" style={{ color: assignedExcelCount === excelCols.length ? "var(--green)" : "var(--amber)" }}>{assignedExcelCount}/{excelCols.length}</div></div>
          <div><div className="tiny muted">Rows ready</div><div className="tiny mono" style={{ color: "var(--green)" }}>{overallOk}</div></div>
          <div><div className="tiny muted">With issues</div><div className="tiny mono"><span style={{ color: "var(--amber,#d97706)" }}>{overallWarn}</span> warn · <span style={{ color: "var(--red)" }}>{overallError}</span> err{!lookups && <span className="muted"> (validating…)</span>}</div></div>
        </div>
      </div>

      {/* Field validation table */}
      <div className="card" style={{ marginBottom: 12, overflow: "hidden" }}>
        <table style={{ borderCollapse: "collapse", width: "100%" }}>
          <thead>
            <tr style={{ background: "var(--paper-2)", borderBottom: "2px solid var(--line)" }}>
              <th style={{ padding: "10px 16px", textAlign: "left", fontSize: 12, fontWeight: 700 }}>Field</th>
              <th style={{ padding: "10px 16px", textAlign: "center", fontSize: 12, fontWeight: 700, color: "var(--green)", width: 110 }}>Valid</th>
              <th style={{ padding: "10px 16px", textAlign: "center", fontSize: 12, fontWeight: 700, color: "var(--amber,#d97706)", width: 110 }}>Warnings</th>
              <th style={{ padding: "10px 16px", textAlign: "center", fontSize: 12, fontWeight: 700, color: "var(--red,#dc2626)", width: 110 }}>Errors</th>
            </tr>
          </thead>
          <tbody>
            {fieldStats.map(({ fieldDef, valid, warn, error }) => {
              const canMap = fieldDef.hasFk;
              const CellBtn = ({ count, colType, color, bg, bd }) => count > 0 ? (
                <button onClick={() => openModal(fieldDef, colType)}
                  style={{ background: bg, border: `1px solid ${bd}`, borderRadius: 5, cursor: "pointer",
                    fontSize: 13, fontWeight: 700, color, padding: "3px 14px", minWidth: 48 }}>
                  {count}{canMap && colType !== "valid" ? " ✎" : ""}
                </button>
              ) : <span className="tiny muted">—</span>;
              return (
                <tr key={fieldDef.key} style={{ borderBottom: "1px solid var(--line)" }}>
                  <td style={{ padding: "11px 16px" }}>
                    <span style={{ fontWeight: 600, fontSize: 13 }}>{fieldDef.label}</span>
                    {fieldDef.required && <span style={{ color: "var(--red)", marginLeft: 4 }}>*</span>}
                    <span className="tiny muted" style={{ marginLeft: 10 }}>{fieldDef.hasFk ? fieldDef.lookupKey : fieldDef.key === "rate_local" ? "numeric" : "date"}</span>
                  </td>
                  <td style={{ padding: "11px 16px", textAlign: "center" }}>
                    <CellBtn count={valid} colType="valid" color="var(--green)" bg="var(--green-soft,#f0fdf4)" bd="var(--green)" />
                  </td>
                  <td style={{ padding: "11px 16px", textAlign: "center" }}>
                    <CellBtn count={warn} colType="warn" color="var(--amber,#d97706)" bg="#fff8e1" bd="#fcd34d" />
                  </td>
                  <td style={{ padding: "11px 16px", textAlign: "center" }}>
                    <CellBtn count={error} colType="error" color="var(--red,#dc2626)" bg="#fef4f4" bd="#fca5a5" />
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>

      {/* Import action card */}
      <div className="card" style={{ padding: "12px 16px" }}>
        <div className="tiny" style={{ fontWeight: 700, color: "var(--teal)", textTransform: "uppercase", letterSpacing: "0.07em", marginBottom: 10 }}>Import Action</div>
        <div style={{ display: "grid", gridTemplateColumns: "14px 1fr 210px", alignItems: "center", gap: "8px 12px" }}>
          <div style={{ width: 10, height: 10, borderRadius: "50%", background: "var(--green)" }} />
          <div><span style={{ fontSize: 13, fontWeight: 500 }}>Valid</span><span className="tiny muted" style={{ marginLeft: 8 }}>{overallOk} rows · all required fields resolved</span></div>
          <div className="tiny" style={{ color: "var(--green)", fontWeight: 500 }}>Import as Active</div>
          <div style={{ width: 10, height: 10, borderRadius: "50%", background: "var(--amber,#f59e0b)" }} />
          <div><span style={{ fontSize: 13, fontWeight: 500 }}>Warnings</span><span className="tiny muted" style={{ marginLeft: 8 }}>optional FK not found in metadata</span></div>
          <select value={warnAction} onChange={e => setWarnAction(e.target.value)}
            style={{ fontSize: 12, padding: "4px 8px", borderRadius: 6, border: "1px solid var(--line)", background: "var(--paper)", cursor: "pointer" }}>
            <option value="skip">Ignore (don't import)</option>
            <option value="draft">Move to Draft</option>
            <option value="export">Export to CSV</option>
          </select>
          <div style={{ width: 10, height: 10, borderRadius: "50%", background: "var(--red,#dc2626)" }} />
          <div><span style={{ fontSize: 13, fontWeight: 500 }}>Errors</span><span className="tiny muted" style={{ marginLeft: 8 }}>missing / invalid required fields</span></div>
          <select value={errorAction} onChange={e => setErrorAction(e.target.value)}
            style={{ fontSize: 12, padding: "4px 8px", borderRadius: 6, border: "1px solid var(--line)", background: "var(--paper)", cursor: "pointer" }}>
            <option value="skip">Ignore (don't import)</option>
            <option value="export">Export to CSV</option>
          </select>
        </div>
      </div>

      {/* ── Inspect / Mapping Modal ── */}
      {inspectModal && (() => {
        const { fieldDef, colType } = inspectModal;
        const canMap = fieldDef.hasFk && colType !== "valid";
        const lookup = fieldDef.hasFk ? lookups?.[fieldDef.lookupKey] : null;

        // Collect distinct raw values for this field that match the clicked colType
        const valGroups = new Map(); // rawVal → count
        for (const { enriched } of processedRows) {
          const rawVal = enriched[fieldDef.key] ?? "";
          const s = evalField(fieldDef, rawVal);
          if (s !== colType) continue;
          valGroups.set(rawVal, (valGroups.get(rawVal) || 0) + 1);
        }

        // Metadata options list (shared, sorted)
        const metaOptions = !lookup ? [] :
          Object.entries(lookup).map(([code, rec]) => ({ code, name: rec.name || rec.iso_code || code }))
            .sort((a, b) => a.name.localeCompare(b.name));

        const colColor = colType === "valid" ? "var(--green)" : colType === "warn" ? "var(--amber,#d97706)" : "var(--red,#dc2626)";
        const colLabel = colType === "valid" ? "Valid" : colType === "warn" ? "Warning" : "Error";
        const totalMatchRows = [...valGroups.values()].reduce((s, n) => s + n, 0);
        const mappedCount = Object.keys(pendingMap).filter(k => valGroups.has(k)).length;

        return (
          <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.45)", zIndex: 2000, display: "flex", alignItems: "center", justifyContent: "center" }}
            onClick={e => e.target === e.currentTarget && closeModal()}>
            <div style={{ background: "#ffffff", borderRadius: 10, boxShadow: "0 10px 50px rgba(0,0,0,0.35)", width: 680, minHeight: 420, maxHeight: "85vh", display: "flex", flexDirection: "column", overflow: "hidden" }}>

              {/* Header */}
              <div style={{ padding: "14px 20px", borderBottom: "1px solid #e5e7eb", background: "#f9fafb", display: "flex", alignItems: "center", gap: 10, flexShrink: 0 }}>
                <span style={{ fontWeight: 700, fontSize: 15 }}>{fieldDef.label}</span>
                <span style={{ fontSize: 11, padding: "2px 8px", borderRadius: 10, background: colType === "valid" ? "#f0fdf4" : colType === "warn" ? "#fff8e1" : "#fef4f4", color: colColor, fontWeight: 600 }}>{colLabel}</span>
                <span style={{ fontSize: 12, color: "#6b7280" }}>{valGroups.size} distinct value{valGroups.size !== 1 ? "s" : ""} · {totalMatchRows} row{totalMatchRows !== 1 ? "s" : ""}</span>
                {canMap && mappedCount > 0 && (
                  <span style={{ fontSize: 11, padding: "2px 8px", borderRadius: 10, background: "#e0f2f1", color: "var(--teal)", fontWeight: 600 }}>
                    {mappedCount} mapped
                  </span>
                )}
                <div style={{ flex: 1 }} />
                <button onClick={closeModal} style={{ background: "none", border: "none", cursor: "pointer", fontSize: 20, color: "#9ca3af", lineHeight: 1, padding: "0 4px" }}>×</button>
              </div>

              {/* Body */}
              <div style={{ flex: 1, overflowY: "auto", minHeight: 260 }}>
                <table style={{ borderCollapse: "collapse", width: "100%" }}>
                  <thead>
                    <tr style={{ background: "#f3f4f6", position: "sticky", top: 0, zIndex: 1 }}>
                      <th style={{ padding: "8px 16px", textAlign: "left", fontSize: 11, fontWeight: 600, color: "#6b7280" }}>Value in file</th>
                      <th style={{ padding: "8px 10px", textAlign: "center", fontSize: 11, fontWeight: 600, color: "#6b7280", width: 60 }}>Rows</th>
                      {canMap && <th style={{ padding: "8px 16px", textAlign: "left", fontSize: 11, fontWeight: 600, color: "#6b7280" }}>Mapped to</th>}
                    </tr>
                  </thead>
                  <tbody>
                    {[...valGroups.entries()].map(([rawVal, count]) => {
                      const mappedCode = pendingMap[rawVal];
                      const resolvedRec = mappedCode && lookup ? lookup[mappedCode] : null;
                      const resolvedName = resolvedRec ? (resolvedRec.name || resolvedRec.iso_code || mappedCode) : mappedCode;
                      return (
                        <tr key={rawVal || "__empty__"} style={{ borderBottom: "1px solid #f3f4f6", verticalAlign: "top",
                          background: mappedCode ? "#f0fdf4" : "transparent" }}>
                          <td style={{ padding: "12px 16px" }}>
                            {rawVal
                              ? <span style={{ fontFamily: "monospace", fontSize: 12 }}>{rawVal}</span>
                              : <span style={{ fontSize: 11, color: "#9ca3af", fontStyle: "italic" }}>(empty)</span>}
                          </td>
                          <td style={{ padding: "12px 10px", textAlign: "center" }}>
                            <span style={{ fontSize: 12, fontWeight: 700, fontFamily: "monospace" }}>{count}</span>
                          </td>
                          {canMap && (
                            <td style={{ padding: "8px 16px" }}>
                              {!rawVal ? (
                                <span style={{ fontSize: 11, color: "#9ca3af" }}>cannot map empty</span>
                              ) : pendingMap[rawVal] !== undefined ? (
                                <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "4px 0" }}>
                                  {pendingMap[rawVal] === "" ? (
                                    <span style={{ fontSize: 12, fontWeight: 600, color: "#9ca3af", fontStyle: "italic" }}>— leave blank</span>
                                  ) : (
                                    <>
                                      <span style={{ fontSize: 12, fontWeight: 600, color: "#16a34a" }}>{resolvedName}</span>
                                      <span style={{ fontFamily: "monospace", fontSize: 10, color: "#9ca3af" }}>({mappedCode})</span>
                                    </>
                                  )}
                                  <button onClick={() => { setPendingMap(prev => { const n = {...prev}; delete n[rawVal]; return n; }); }}
                                    style={{ background: "none", border: "none", cursor: "pointer", color: "#9ca3af", fontSize: 15, padding: 0, lineHeight: 1, marginLeft: 2 }}>×</button>
                                </div>
                              ) : (
                                <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                                  <div style={{ flex: 1 }}>
                                    <SearchSelect
                                      options={metaOptions}
                                      value=""
                                      onChange={code => setPendingMap(prev => ({ ...prev, [rawVal]: code }))}
                                      placeholder={`Search ${fieldDef.label}…`}
                                      getLabel={o => o.name || o.code}
                                      getValue={o => o.code}
                                    />
                                  </div>
                                  {!fieldDef.required && (
                                    <button
                                      onClick={() => setPendingMap(prev => ({ ...prev, [rawVal]: "" }))}
                                      title="Leave this field blank on import"
                                      style={{ flexShrink: 0, fontSize: 11, color: "#6b7280", background: "none", border: "1px solid #d1d5db", borderRadius: 4, padding: "3px 8px", cursor: "pointer", whiteSpace: "nowrap" }}>
                                      Leave blank
                                    </button>
                                  )}
                                </div>
                              )}
                            </td>
                          )}
                        </tr>
                      );
                    })}
                  </tbody>
                </table>
              </div>

              {/* Footer */}
              <div style={{ padding: "12px 20px", borderTop: "1px solid #e5e7eb", background: "#f9fafb", display: "flex", gap: 10, justifyContent: "flex-end", alignItems: "center", flexShrink: 0 }}>
                {canMap && <span style={{ fontSize: 11, color: "#9ca3af", marginRight: "auto" }}>Mapping will be applied on import.</span>}
                <button className="btn" onClick={closeModal}>Cancel</button>
                {canMap && (
                  <button className="btn teal"
                    disabled={mappedCount === 0}
                    onClick={commitMapping}>
                    Apply & Re-validate ({mappedCount})
                  </button>
                )}
                {!canMap && <button className="btn teal" onClick={closeModal}>Close</button>}
              </div>
            </div>
          </div>
        );
      })()}
    </div>
  );
}

Object.assign(window, { RateList, RateDetail, AddRateForm, ImportWizard, SearchSelect });
