// T2 Falcon Admin — Wallet & Balance Management page
// 3-panel layout: Falcon Clients (left) · Settings card (center) · Allocation table (right)
// Drives Single↔Multiple wallets, Node↔User based, and a slide-in Balance Transfer drawer.

const { useState: useStateW, useEffect: useEffectW, useMemo: useMemoW, useRef: useRefW } = React;

// ====== Channels (only used in Multiple Wallets) ======
const WB_CHANNELS = [
  { id: 'whatsapp', label: 'wbWhatsapp', tone: '#25D366', icon: 'whatsapp' },
  { id: 'voice',    label: 'wbVoice',    tone: '#3B82F6', icon: 'voice' },
  { id: 'aichat',   label: 'wbAiChat',   tone: '#8B5CF6', icon: 'ai' },
  { id: 'sms',      label: 'wbSms',      tone: '#F59E0B', icon: 'sms' },
  { id: 'email',    label: 'wbEmail',    tone: '#EF4444', icon: 'email' },
];

// ====== Channel mini icons ======
const ChannelIcon = ({ kind, size = 14 }) => {
  const c = { width: size, height: size, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round' };
  if (kind === 'whatsapp') return <svg {...c}><path d="M8 1.5a6.5 6.5 0 0 0-5.6 9.8L1.5 14.5l3.3-.9A6.5 6.5 0 1 0 8 1.5Z" /><path d="M5.6 6c.3 1.6 2 3.5 4 4l.7-1c.2-.3.5-.4.8-.3l1.2.4c.3.1.4.3.4.6 0 1-.7 1.7-1.7 1.7-3 0-6-3-6-6 0-1 .8-1.7 1.7-1.7.3 0 .5.1.6.4l.4 1.2c.1.3 0 .6-.3.8L5.6 6Z" /></svg>;
  if (kind === 'voice')    return <svg {...c}><path d="M3 4.4c0-.8.6-1.4 1.4-1.4h1.7c.4 0 .8.3.9.7l.6 2c.1.4 0 .8-.3 1l-1.1.7a8 8 0 0 0 4.4 4.4l.7-1.1c.2-.3.6-.4 1-.3l2 .6c.4.1.7.5.7.9V13a1.4 1.4 0 0 1-1.4 1.4A11.4 11.4 0 0 1 3 4.4Z" /></svg>;
  if (kind === 'ai')       return <svg {...c}><path d="M3 5.5c0-.8.7-1.5 1.5-1.5H8l3 3v3.5c0 .8-.7 1.5-1.5 1.5H8l-2.5 2v-2H4.5C3.7 12 3 11.3 3 10.5v-5Z" /><path d="M6 7h3M6 9h2" /></svg>;
  if (kind === 'sms')      return <svg {...c}><path d="M2 4.5C2 3.7 2.7 3 3.5 3h9c.8 0 1.5.7 1.5 1.5v6c0 .8-.7 1.5-1.5 1.5H6l-3 2.5V12c-.6 0-1-.4-1-1V4.5Z" /></svg>;
  if (kind === 'email')    return <svg {...c}><rect x="2" y="4" width="12" height="9" rx="1.5" /><path d="m2.5 5 5.5 4 5.5-4" /></svg>;
  return null;
};

// ====== Currency helpers ======
// Always: comma thousands separators, a period decimal point, and 4 decimal
// places (e.g. 965258 -> "965,258.0000", 20000000000 -> "20,000,000,000.0000").
// Accepts numbers OR strings like "10,000" (commas stripped before parsing).
const fmtNum = (n) => {
  const num = typeof n === 'number' ? n : parseFloat(String(n == null ? '' : n).replace(/,/g, ''));
  if (!isFinite(num)) return '0.0000';
  return num.toLocaleString('en-US', { minimumFractionDigits: 4, maximumFractionDigits: 4 });
};
// For big totals like the Master Wallet: comma grouping, no forced decimals
// (e.g. 20000000000 -> "20,000,000,000"), shows decimals only if present.
const fmtTotal = (n) => {
  const num = typeof n === 'number' ? n : parseFloat(String(n == null ? '' : n).replace(/,/g, ''));
  if (!isFinite(num)) return '0';
  return num.toLocaleString('en-US', { maximumFractionDigits: 4 });
};
const parseNum = (s) => {
  const v = parseInt(String(s).replace(/[^\d-]/g, ''), 10);
  return isNaN(v) ? 0 : v;
};

// ====== Currency mark — proxies to either IcRiyal (SAR) or IcPoints (Pt) ======
// All amount cells/glyphs in the wallet page use this so a single currency
// toggle (Settings → Currency) flips every ﷼ icon to a "Pt" coin and back.
// Accepts the explicit `currency` prop OR falls back to a global mirror so
// nested rows / the transfer drawer pick up the right glyph automatically.
// Matches "Points", "PT", "pt", or any string starting with "p" — keeps the
// switch robust to variant casing in different dropdowns.
const RiyalMark = ({ size = 14, currency }) => {
  const c = (currency || (typeof window !== 'undefined' && window.__wbCurrency) || '').toString();
  return /^p/i.test(c) ? <IcPoints size={size} /> : <IcRiyal size={size} />;
};

// ====== Transfer / exchange icon used in row buttons ======
const TransferIcon = ({ size = 16 }) => (
  <svg width={size} height={size} viewBox="0 0 27 27" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden>
    <path d="M23 14H25C25 20.617 19.617 26 13 26C6.383 26 1 20.617 1 14C1 7.383 6.383 2 13 2V4C7.486 4 3 8.486 3 14C3 19.514 7.486 24 13 24C18.514 24 23 19.514 23 14ZM12 8V9C10.346 9 9 10.346 9 12C9 13.359 9.974 14.51 11.315 14.733L14.355 15.239C14.729 15.301 15 15.621 15 16C15 16.552 14.552 17 14 17H12C11.448 17 11 16.552 11 16H9C9 17.654 10.346 19 12 19V20H14V19C15.654 19 17 17.654 17 16C17 14.641 16.026 13.49 14.685 13.267L11.645 12.761C11.271 12.699 11 12.379 11 12C11 11.448 11.448 11 12 11H14C14.552 11 15 11.448 15 12H17C17 10.346 15.654 9 14 9V8H12ZM23 2H18V4H21.586L17.293 8.293L18.707 9.707L23 5.414V9H25V4C25 2.897 24.103 2 23 2Z"/>
  </svg>
);

// ====== User avatar ======
const UserDot = ({ name }) => {
  const init = (name || '?').trim().charAt(0).toUpperCase();
  return (
    <span className="wb-user-dot" aria-hidden>
      <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
        <circle cx="8" cy="6" r="3" />
        <path d="M2.5 14c.8-2.6 3-4 5.5-4s4.7 1.4 5.5 4" />
      </svg>
    </span>
  );
};

// ====== Tree connector rails ======
// Draws the same elbow / vertical guide lines as the Organization Hierarchy
// clients tree (see .tree-rail* in styles.css). Each row carries `depth`,
// `ancestorsHasNext` (does each ancestor level continue below?) and
// `isLastChild` (last among its siblings → closing └ elbow instead of ├).
// Shared by BOTH wallet views (Show as Falcon + Show as Client) — defined once
// here, same cross-file pattern as UserDot.
const wbTreeRails = (row, trace, rowIdx, showHead) => {
  if (!row) return null;
  const anc = row.ancestorsHasNext || [];
  const rails = [];
  for (let i = 0; i < row.depth; i++) {
    const isElbow = i === row.depth - 1;          // last column = elbow to the parent
    const continuesDown = !!anc[i];                // ancestor at this level still has siblings below
    // Hover trace: column i sits under the ancestor at depth i (row.path[i]).
    // It's on the hovered row's vertical spine when the hovered row shares that
    // same ancestor AND this row sits at-or-above where that column's spine ends
    // (the hovered path node at depth i+1 → trace.maxIdx[i]). This lights the
    // continuous vertical link between the hovered child and its parent, drawn
    // through the intermediate sibling rows the spine passes over.
    const onTrace = !!(trace
      && i < trace.path.length - 1
      && row.path && row.path[i] === trace.path[i]
      && typeof rowIdx === 'number'
      && rowIdx <= trace.maxIdx[i]);
    rails.push(
      <span
        key={i}
        aria-hidden="true"
        className={`tree-rail ${isElbow ? 'elbow' : ''} ${!continuesDown && !isElbow ? 'rail-empty' : ''} ${isElbow && row.isLastChild && !showHead ? 'rail-last' : ''} ${onTrace ? 'wb-trace' : ''}`}
      />
    );
  }
  // When this node is expanded, drop a short connector from its OWN row (centre
  // → bottom) into its children's lane, so the spine visibly starts AT the node
  // — making it clear the rows below (sub-nodes / users) belong to it. It lands
  // in the chevron's lane, so a negative inline-end margin lets it overlay the
  // chevron without shifting the layout. Applies to every node (depth 0 too).
  if (showHead) {
    rails.push(<span key="head" aria-hidden="true" className="tree-rail rail-head" />);
  }
  if (!rails.length) return null; // depth-0 header with nothing to draw
  return <span className="tree-rails wb-tree-rails">{rails}</span>;
};

// ====== Seed allocation values per node id ======
// Millions-scale samples: up to 9 digits before the dot, 4 digits after (e.g.
// 765,432,109.8765). Stored as JS numbers — exact to 4 decimals at this scale.
const SEED_ALLOC = {
  // Realistic large-enterprise messaging budgets (SAR). 4-decimal remaining
  // balances reflect partial consumption.
  aramco:    { single: 0,              whatsapp: 18_450_320.5500, voice: 12_280_140.2500, aichat: 9_820_000.0000, sms: 6_540_220.7500, email: 2_180_000.0000 },
  hr:        { single: 3_240_180.5500, whatsapp: 1_620_090.2500,  voice: 980_140.7500,    aichat: 540_000.0000,   sms: 320_180.5000,   email: 120_040.2500 },
  digital:   { single: 5_680_420.2500, whatsapp: 2_840_210.1250,  voice: 1_720_140.5000,  aichat: 980_320.7500,   sms: 480_120.2500,   email: 180_080.5000 },
  cc:        { single: 8_920_640.7500, whatsapp: 4_460_320.3750,  voice: 2_680_180.2500,  aichat: 1_240_140.5000, sms: 720_220.7500,   email: 260_120.2500 },
  inbound:   { single: 4_180_220.5000, whatsapp: 2_090_110.2500,  voice: 1_280_080.5000,  aichat: 620_040.7500,   sms: 340_160.2500,   email: 140_060.5000 },
  outbound:  { single: 3_620_140.2500, whatsapp: 1_810_070.1250,  voice: 1_120_060.5000,  aichat: 520_040.2500,   sms: 280_120.7500,   email: 110_040.2500 },
  care:      { single: 2_980_180.7500, whatsapp: 1_490_090.3750,  voice: 920_080.2500,    aichat: 420_040.5000,   sms: 240_120.2500,   email: 90_040.7500 },
  marketing: { single: 9_840_520.5000, whatsapp: 4_920_260.2500,  voice: 2_950_180.5000,  aichat: 1_480_140.7500, sms: 980_320.2500,   email: 360_180.5000 },
  itsec:     { single: 1_420_080.2500, whatsapp: 710_040.1250,    voice: 440_020.5000,    aichat: 180_010.7500,   sms: 120_060.2500,   email: 40_020.5000 },
  alrajhi:   { single: 14_280_640.5000, whatsapp: 7_140_320.2500, voice: 4_280_180.5000,  aichat: 1_980_140.7500, sms: 1_420_080.2500, email: 480_120.5000 },
  snb:       { single: 9_820_440.7500, whatsapp: 4_910_220.3750,  voice: 2_940_180.2500,  aichat: 1_360_120.5000, sms: 980_080.2500,   email: 360_040.7500 },
  bupa:      { single: 4_720_180.2500, whatsapp: 2_360_090.1250,  voice: 1_420_080.5000,  aichat: 640_040.7500,   sms: 420_120.2500,   email: 120_040.5000 },
};

// User-level seed — millions-scale (up to 9 digits before the dot, 4 after). Keep
// the decimals (no Math.round) so user wallets also show 4-decimal samples.
const USER_SEED = [
  840_320.5500, 620_180.2500, 480_140.7500, 360_120.5000,
  280_080.2500, 180_060.7500, 120_040.5000, 90_020.2500,
];
const seedUserAlloc = (orgId, userIdx) => {
  const base = USER_SEED[userIdx % USER_SEED.length];
  return {
    single:   base,
    whatsapp: base * 0.45,
    voice:    base * 0.30,
    aichat:   base * 0.18,
    sms:      base * 0.05,
    email:    base * 0.02,
  };
};

// ====== Wallet Page ======
const WalletPage = ({
  tree,
  selected,
  selectNode,
  expanded,
  toggleExpand,
  lang, t,
  pushToast,
}) => {
  // viewAs: null (picker) | 'falcon' | 'client'
  const [viewAs, setViewAs] = useStateW(null);

  // Currently inspected client (default Aramco for parity with reference design)
  const selectedNode = useMemoW(() => {
    const sel = findNode(tree, selected);
    if (sel && sel.type === 'client') return sel;
    return findNode(tree, 'aramco') || tree.children[0];
  }, [tree, selected]);

  // Settings state
  const [currency, setCurrency] = useStateW('SAR');
  // Mirror the currency on a global so deeply nested rows (and the transfer
  // drawer) can pick the right glyph without prop-drilling through every
  // table row / drawer component.
  useEffectW(() => {
    if (typeof window !== 'undefined') window.__wbCurrency = currency;
  }, [currency]);
  // When the currency is "Points", the user chooses which commchannel's rate
  // card drives the calculation. Defaults to WhatsApp to match the design.
  const [rateChannel, setRateChannel] = useStateW('whatsapp');
  const [balanceType, setBalanceType] = useStateW('node');     // node | user
  const [walletType, setWalletType]   = useStateW('single');   // single | multiple
  const [activeChannels, setActiveChannels] = useStateW(['whatsapp', 'voice', 'aichat']);

  // Master pool & allocations
  const [masterTotal] = useStateW(48_756_318.4250);   // realistic large-enterprise master wallet (SAR)
  const [allocations, setAllocations]           = useStateW(() => clone(SEED_ALLOC));
  const [savedAllocations, setSavedAllocations] = useStateW(() => clone(SEED_ALLOC));
  const [userAllocs, setUserAllocs]             = useStateW({}); // { [orgId-uid]: {single, whatsapp, ...} }

  // Drawer
  const [drawerFor, setDrawerFor] = useStateW(null); // {id, kind:'org'|'user', name, parentName?}

  // RBAC role (demo: switchable via the role chip in the table actions row)
  // Roles: 'falcon-admin' | 'account-owner' | 'node-admin' | 'normal-user'
  const [role, setRole] = useStateW('falcon-admin');
  const canTransferMaster = role === 'falcon-admin' || role === 'account-owner';
  const canTransferRows   = role !== 'normal-user';

  // View / Edit mode (Falcon view only) — view mode shows amounts as plain
  // info with a Riyal glyph, edit mode swaps them for editable inputs.
  // No more view/edit toggle: the page opens editable. After Save, fields
  // lock until the user leaves and re-enters the page (selectedNode change
  // resets the flag, see effect below).
  const [wbSaved, setWbSaved] = useStateW(false);
  const isEditing = !wbSaved;
  // Reset saved state whenever the user changes which client they're viewing —
  // effectively "leave & return" resets edit access.
  useEffectW(() => { setWbSaved(false); setConfirmSaveOpen(false); }, [selectedNode?.id]);

  // ----- Confirm-save modal (Show as Falcon) -----
  // Clicking Save does NOT commit immediately — it opens a confirmation
  // dialog so destructive/irreversible saves require an explicit second tap.
  const [confirmSaveOpen, setConfirmSaveOpen] = useStateW(false);
  const requestSave = () => { if (!wbSaved) setConfirmSaveOpen(true); };
  const confirmSave = () => {
    setConfirmSaveOpen(false);
    setWbSaved(true);
    onSave && onSave();
    pushToast && pushToast(t.wbSavedToast || 'Changes saved');
  };
  const cancelSave  = () => setConfirmSaveOpen(false);
  // ESC closes the dialog without saving
  useEffectW(() => {
    if (!confirmSaveOpen) return;
    const onKey = (e) => { if (e.key === 'Escape') cancelSave(); };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [confirmSaveOpen]);
  // Falcon System Admin: per-channel sub-balances under Master Wallet — same
  // view/edit dance as the rest of the table.
  const [masterSubBalances, setMasterSubBalances] = useStateW({
    whatsapp: 18_450_320.5500, voice: 12_280_140.2500, aichat: 9_820_000.0000,
    sms: 6_540_220.7500, email: 2_180_000.0000,
  });
  // (Pagination state removed — hierarchy tree shows all expanded rows;
  // pagination over a tree doesn't have a clean definition, and the
  // pagination footer was the only thing producing a second vertical scroll
  // inside the table card.)

  // Local row expansion (independent of sidebar tree expansion so the page is self-contained)
  const [rowOpen, setRowOpen] = useStateW({}); // first-level only — values true|false
  // Hover-path highlight: Set of node ids on the hovered row's ancestor chain —
  // drives the teal rail highlight, same behaviour as the Hierarchy tree.
  const [wbHoveredPath, setWbHoveredPath] = useStateW(null);
  // Shared row hover: hovering a row in EITHER card lights up the matching row
  // (light grey) on BOTH sides, so the eye can track the row across the split.
  const [wbHoveredRow, setWbHoveredRow] = useStateW(null);

  // ----- Resizable Organizations | Wallet split -----
  // Drag the divider between the two cards to set the Organizations pane width.
  // Width lives in state (persists while on the page); null → CSS default (272px).
  // Each card keeps its OWN internal scrollbars at any width, so narrowing the
  // pane just reveals its horizontal scrollbar — nothing is clipped.
  const wbSplitRef = useRefW(null);
  const [orgPaneW, setOrgPaneW] = useStateW(null);
  const [wbResizing, setWbResizing] = useStateW(false);
  const wbResizerRef = useRefW(null);
  const wbGripRef = useRefW(null);
  // ===== One vertical scroll for both panes (guaranteed row match) =====
  // The Wallet (right) pane is the ONLY vertical scroller. The Organizations
  // (left) pane has overflow-y:hidden and its body is TRANSLATED to mirror the
  // Wallet scrollTop EXACTLY, so the two row-stacks are mathematically locked
  // and can never drift apart. React owns the scroll handler + element refs
  // (via onScroll + ref below), so it always binds to the CURRENT elements —
  // this is what makes it survive leaving the page and coming back: a plain
  // querySelector+addEventListener effect could bind to a stale node after the
  // component remounted, which is why scrolling drifted after exit/return.
  const wbValScrollRef = useRefW(null);
  const wbOrgStackRef  = useRefW(null);
  const wbScrollingRef = useRefW(false);   // true while the Wallet pane is actively scrolling
  const wbScrollRafRef = useRefW(null);
  const wbScrollEndRef = useRefW(null);
  const mirrorOrgToWallet = () => {
    const s = wbValScrollRef.current, c = wbOrgStackRef.current;
    if (s && c) c.style.transform = `translateY(${-s.scrollTop}px)`;
  };
  // Fired on every Wallet-pane scroll. During an active (fast) scroll we:
  //  1. drive the org mirror every animation FRAME via rAF, so it tracks the
  //     compositor scroll tightly instead of lagging behind throttled/coalesced
  //     scroll events; and
  //  2. drop any row hover and ignore new hovers — this kills the mouseenter
  //     re-render storm (rows sweeping under a stationary cursor) that would
  //     otherwise stall the mirror, AND guarantees a highlight is NEVER painted
  //     on a momentarily-drifted row. The highlight can therefore only appear at
  //     rest, where the two panes are pixel-aligned — one clean line across both.
  const onWalletScroll = () => {
    mirrorOrgToWallet();
    if (!wbScrollingRef.current) {
      wbScrollingRef.current = true;
      setWbHoveredRow(null);
      setWbHoveredPath(null);
      const loop = () => {
        mirrorOrgToWallet();
        if (wbScrollingRef.current) wbScrollRafRef.current = requestAnimationFrame(loop);
      };
      wbScrollRafRef.current = requestAnimationFrame(loop);
    }
    clearTimeout(wbScrollEndRef.current);
    wbScrollEndRef.current = setTimeout(() => {
      wbScrollingRef.current = false;
      if (wbScrollRafRef.current) cancelAnimationFrame(wbScrollRafRef.current);
      mirrorOrgToWallet();   // final, exact sync once scrolling settles
    }, 120);
  };
  const onOrgWheel = (e) => {
    const s = wbValScrollRef.current;
    if (s && Math.abs(e.deltaY) >= Math.abs(e.deltaX) && e.deltaY !== 0) {
      s.scrollTop += e.deltaY;   // org pane has no vertical scroll of its own
      onWalletScroll();          // same path: mirror + rAF + hover-suppress
    }
  };
  // Cancel any in-flight scroll rAF / timer if the component unmounts mid-scroll.
  useEffectW(() => () => {
    if (wbScrollRafRef.current) cancelAnimationFrame(wbScrollRafRef.current);
    clearTimeout(wbScrollEndRef.current);
  }, []);
  // The resize grip is position:fixed at the vertical CENTER of the viewport so
  // it stays visible while the rows scroll; keep it horizontally over the divider.
  const placeGrip = () => {
    const rz = wbResizerRef.current, g = wbGripRef.current;
    if (!rz || !g) return;
    const r = rz.getBoundingClientRect();
    g.style.left = Math.round(r.left + r.width / 2) + 'px';
  };
  useEffectW(() => {
    placeGrip();
    const raf = requestAnimationFrame(placeGrip);
    const onWin = () => placeGrip();
    window.addEventListener('resize', onWin);
    return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', onWin); };
  }, [orgPaneW, viewAs]);
  const ORG_MIN = 160;
  const orgMaxW = () => {
    const c = wbSplitRef.current;
    return c ? Math.max(ORG_MIN, c.getBoundingClientRect().width - 260) : 640;
  };
  const startOrgResize = (startEv) => {
    startEv.preventDefault();
    const c = wbSplitRef.current;
    if (!c) return;
    const rect = c.getBoundingClientRect();
    setWbResizing(true);
    document.body.style.userSelect = 'none';
    document.body.style.cursor = 'col-resize';
    const onMove = (ev) => {
      if (ev.cancelable) ev.preventDefault();
      const clientX = ev.touches ? ev.touches[0].clientX : ev.clientX;
      const max = Math.max(ORG_MIN, rect.width - 260);
      setOrgPaneW(Math.round(Math.min(max, Math.max(ORG_MIN, clientX - rect.left))));
    };
    const stop = () => {
      setWbResizing(false);
      document.body.style.userSelect = '';
      document.body.style.cursor = '';
      document.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseup', stop);
      document.removeEventListener('touchmove', onMove);
      document.removeEventListener('touchend', stop);
    };
    document.addEventListener('mousemove', onMove);
    document.addEventListener('mouseup', stop);
    document.addEventListener('touchmove', onMove, { passive: false });
    document.addEventListener('touchend', stop);
  };
  const onOrgResizeKey = (e) => {
    if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
    e.preventDefault();
    const base = orgPaneW || 272;
    const next = e.key === 'ArrowLeft' ? base - 24 : base + 24;
    setOrgPaneW(Math.round(Math.min(orgMaxW(), Math.max(ORG_MIN, next))));
  };
  useEffectW(() => {
    // Auto-open the client itself + Contact Center + IT & Cybersecurity for the demo
    if (selectedNode?.id === 'aramco') {
      setRowOpen(o => ({ aramco: true, cc: true, itsec: true, ...o }));
    } else if (selectedNode) {
      setRowOpen(o => ({ [selectedNode.id]: true, ...o }));
    }
  }, [selectedNode?.id]);

  const toggleRow = (id) => setRowOpen(o => ({ ...o, [id]: !o[id] }));

  // Re-sync the org body's mirror transform after anything that rebuilds the row
  // list or remounts the table (selection, wallet/balance mode, channels, expand/
  // collapse). The LIVE scroll is handled by onScroll on the Wallet pane (React-
  // owned, see refs above) — this effect only covers the non-scroll cases, e.g.
  // landing back on the page where the Wallet pane is freshly at scrollTop 0.
  useEffectW(() => { mirrorOrgToWallet(); },
    [selectedNode?.id, walletType, balanceType, activeChannels.length, rowOpen, viewAs]);

  // ----- Build display rows (recursive — supports up to 5 levels of nesting) -----
  // The selected client is the top header row (depth 0, no input).
  // Children expand recursively; each row gets +1 depth indent.
  const MAX_DEPTH = 20; // allow deep trees (BMW Group has up to 10 levels)
  const tableRows = useMemoW(() => {
    if (!selectedNode) return [];
    const rows = [];

    const visit = (org, depth, parentName, ancestorsHasNext, isLastChild, parentPath) => {
      const kids = org.children || [];
      const users = (org.users || []).slice(0, 3);
      const canDescend = depth < MAX_DEPTH;
      const hasChildren = canDescend && (
        balanceType === 'user'
          ? (kids.length > 0 || users.length > 0)
          : kids.length > 0
      );
      const path = [...parentPath, org.id];   // root→self id chain (hover-path highlight)

      rows.push({
        id: org.id,
        depth,
        name: org.name,
        kind: 'org',
        isHeader: depth === 0,    // the selected client itself — no input
        hasChildren,
        parentName,
        ancestorsHasNext,         // tree-rail metadata: does each ancestor level continue below?
        isLastChild,              // last among siblings → closing └ elbow
        path,
      });

      if (!rowOpen[org.id] || !canDescend) return;

      // Sub-nodes and (user-mode) the node's OWN users are one ordered sibling
      // list. This way each user's rail elbows back to THIS node — making it
      // unambiguous which users sit directly under the node vs. under a sub-node
      // (e.g. Contact Center's users vs. Customer Care's users) — and the very
      // last item gets the closing └ elbow.
      // depth 0 is the client HEADER (it has no rail of its own), so it must NOT
      // contribute a rail column — otherwise every descendant carries a phantom
      // leading lane and the vertical guide breaks through expanded sub-trees.
      // For every other (expanded) node we pass `true`: its own lane CONTINUES
      // down alongside its children. For a non-last node that's the normal line
      // to its next sibling; for the LAST child it becomes the OUTER bracket that
      // runs down the left of its expanded sub-tree and ends at the last
      // descendant (paired with dropping rail-last on its own elbow when open).
      const childAncestors = depth === 0 ? ancestorsHasNext : [...ancestorsHasNext, true];
      // User Based: list the node's OWN users FIRST, then the sub-nodes (more
      // useful — you see the people directly under a node before drilling into
      // the nodes beneath it). Node Based: sub-nodes only.
      const childItems = balanceType === 'user'
        ? [
            ...users.map((u, idx) => ({ type: 'user', u, idx })),
            ...kids.map(k => ({ type: 'org', node: k })),
          ]
        : [...kids.map(k => ({ type: 'org', node: k }))];
      childItems.forEach((item, j) => {
        const childIsLast = j === childItems.length - 1;
        if (item.type === 'org') {
          visit(item.node, depth + 1, org.name, childAncestors, childIsLast, path);
        } else {
          const userRowId = `${org.id}::${item.u.id}`;
          rows.push({
            id: userRowId,
            depth: depth + 1,
            name: item.u.firstName || item.u.username,
            kind: 'user',
            orgId: org.id,
            userIdx: item.idx,
            hasChildren: false,
            parentName: org.name,
            ancestorsHasNext: childAncestors,
            isLastChild: childIsLast,
            path: [...path, userRowId],
          });
        }
      });
    };

    visit(selectedNode, 0, null, [], true, []);
    return rows;
  }, [selectedNode, balanceType, rowOpen]);

  // ----- Hover connector trace -----
  // When a row is hovered, light the continuous VERTICAL spine that links it to
  // its parent (and on up its ancestor chain) — including the segments that pass
  // THROUGH the intermediate sibling rows. `.on-path` only paints the ancestor
  // rows' own rails, which leaves gaps; this fills them. For each ancestor depth
  // c we record maxIdx[c] = the render-index of the hovered row's path node at
  // depth c+1, i.e. where that column's spine ends (so it stops at the hovered
  // child and doesn't bleed into siblings below it).
  const wbTrace = useMemoW(() => {
    if (!wbHoveredRow) return null;
    const hr = tableRows.find(r => r.id === wbHoveredRow);
    if (!hr || !hr.path || !hr.depth) return null;
    const idxById = new Map();
    tableRows.forEach((r, i) => idxById.set(r.id, i));
    const maxIdx = hr.path.slice(1).map(id => {
      const v = idxById.get(id);
      return v === undefined ? -1 : v;
    });
    return { path: hr.path, maxIdx };
  }, [wbHoveredRow, tableRows]);

  // ----- Get/set allocation for a row -----
  const getAlloc = (row, key) => {
    if (row.kind === 'user') {
      const k = `${row.orgId}::${row.userIdx}`;
      const u = userAllocs[k] || seedUserAlloc(row.orgId, row.userIdx);
      return u[key] || 0;
    }
    return allocations[row.id]?.[key] || 0;
  };
  const setAlloc = (row, key, val) => {
    const v = parseNum(val);
    if (row.kind === 'user') {
      const k = `${row.orgId}::${row.userIdx}`;
      const cur = userAllocs[k] || seedUserAlloc(row.orgId, row.userIdx);
      setUserAllocs(u => ({ ...u, [k]: { ...cur, [key]: v } }));
    } else {
      setAllocations(a => ({ ...a, [row.id]: { ...(a[row.id] || {}), [key]: v } }));
    }
  };

  // ----- Save / Cancel -----
  const onSave = () => {
    setSavedAllocations(clone(allocations));
    pushToast(t.wbToastSaved);
  };
  const onCancel = () => {
    setAllocations(clone(savedAllocations));
    pushToast(t.wbToastReset);
  };

  // ----- Channel toggle (Multiple Wallets) -----
  const toggleChannel = (id) => {
    // No cap — every channel (WhatsApp, Voice, AI-ChatGPT, SMS, Email) can be toggled on/off freely.
    setActiveChannels(arr => arr.includes(id) ? arr.filter(x => x !== id) : [...arr, id]);
  };

  // ----- Open drawer -----
  const openDrawer = (row) => setDrawerFor(row);

  // ----- Confirm transfer -----
  const onConfirmTransfer = ({ sourceId, sourceCh, destId, destCh, amount }) => {
    // Decrement source, increment destination, in current view's allocation map.
    const apply = (id, ch, delta) => {
      if (!id) return;                                 // Master Wallet — untracked source/sink pool
      if (id.indexOf('__ch_') === 0) {                 // Master's comm-channel pool (sub-balance)
        const c = id.slice(5);
        setMasterSubBalances(prev => ({ ...prev, [c]: Math.max(0, (Number(prev[c]) || 0) + delta) }));
        return;
      }
      // Detect if id encodes a user
      if (id.includes('::')) {
        const [orgId, uid] = id.split('::');
        // Find the user's index in tableRows / selectedNode children
        const org = (selectedNode.children || []).find(o => o.id === orgId);
        const idx = (org?.users || []).findIndex(u => u.id === uid);
        const k = `${orgId}::${Math.max(0, idx)}`;
        const cur = userAllocs[k] || seedUserAlloc(orgId, Math.max(0, idx));
        setUserAllocs(u => ({ ...u, [k]: { ...cur, [ch]: Math.max(0, (cur[ch] || 0) + delta) } }));
      } else {
        setAllocations(a => ({ ...a, [id]: { ...(a[id] || {}), [ch]: Math.max(0, (a[id]?.[ch] || 0) + delta) } }));
      }
    };
    apply(sourceId, sourceCh, -amount);
    apply(destId, destCh, amount);
    pushToast(t.wbToastTransferDone);
    setDrawerFor(null);
  };

  // Channel-mode column count drives grid template.
  const colsKey = walletType === 'single' ? 'single' : `multi-${activeChannels.length}`;

  // ---------- Picker (first screen) ----------
  if (viewAs === null) {
    return <WbViewAsPicker t={t} onPick={(v) => setViewAs(v)} />;
  }

  // ---------- Show as Client (single client perspective) ----------
  if (viewAs === 'client') {
    return (
      <ClientWalletView
        selectedNode={selectedNode}
        tree={tree}
        walletType={walletType} setWalletType={setWalletType}
        balanceType={balanceType}
        activeChannels={activeChannels} toggleChannel={toggleChannel}
        allocations={allocations} setAllocations={setAllocations}
        userAllocs={userAllocs} setUserAllocs={setUserAllocs}
        savedAllocations={savedAllocations} setSavedAllocations={setSavedAllocations}
        rowOpen={rowOpen} setRowOpen={setRowOpen}
        toggleRow={toggleRow}
        drawerFor={drawerFor} setDrawerFor={setDrawerFor}
        onConfirmTransfer={onConfirmTransfer}
        t={t}
        pushToast={pushToast}
        onSwitchView={() => setViewAs(null)}
      />
    );
  }

  return (
    <div className="page wb-page-2col">
      {/* ============== LEFT: Clients tree (full-height, matches Hierarchy) ============== */}
      <TmClientsTree
        tree={tree}
        selected={selectedNode?.id}
        onSelect={(id) => selectNode(id)}
        t={t}
      />
      {/* ============== RIGHT: white content-panel holding settings + allocation table ============== */}
      <div className="content-panel">
        <div className="content-body wb-content-body">

      {/* ============== TOP BAR — spans full width ==============
          Layout: client logo + name on the left, "Viewing as" role selector
          in the middle, "Switch perspective" and "Save" buttons on the right.
          Pulled out of both columns so the page reads as a single header row
          above the table+settings split. */}
      <div className="wb-top-bar">
        <div className="wb-tb-client">
          <BrandLogo brand={selectedNode?.brand} size={32} />
          <span className="wb-tb-client-name">{selectedNode?.name}</span>
        </div>
        <div className="wb-tb-viewing">
          <span className="wb-tb-viewing-label">{(t.wbViewingAs || 'VIEWING AS').toUpperCase()}</span>
          <div className="wb-role-chip wb-tb-viewing-chip">
            <select className="wb-role-chip-select" value={role} onChange={(e) => setRole(e.target.value)}>
              <option value="falcon-admin">{t.wbRoleFalconAdmin || 'Falcon System Admin'}</option>
              <option value="account-owner">{t.wbRoleOwner || 'Account Owner'}</option>
              <option value="node-admin">{t.wbRoleNodeAdmin || 'Node Admin'}</option>
              <option value="normal-user">{t.wbRoleNormalUser || 'Normal User'}</option>
            </select>
          </div>
        </div>
        <div className="wb-tb-actions">
          <button className="btn btn-secondary" onClick={() => setViewAs(null)}>
            <IcArrowLeft size={12} stroke={1.7} /> {t.tplBackToPicker || 'Switch perspective'}
          </button>
          {!wbSaved && (
            <button className="btn btn-primary" onClick={requestSave}>{t.save || 'Save'}</button>
          )}
        </div>
      </div>

      {/* ============== BODY: settings + allocation table ============== */}
      <div className="wb-body-row">

      {/* ============== CENTER: Settings card ============== */}
      <section className="wb-settings-col">
        {/* Master Wallet card — new layout per spec:
              - Master Wallet label + large amount on the left, Transfer button
                on the right (no separate centered title).
              - Horizontal divider below the master row.
              - Per-channel sub-balances each rendered as: small label, then a
                full-width bordered input row showing the value with the
                currency glyph on the left, and the Transfer button OUTSIDE
                the input on the right. Input is always boxed (no view/edit
                visual swap). */}
        <div className="wb-master-card">
          <div className="wb-master-row">
            <div className="wb-master-row-left">
              <div className="wb-master-label">{t.wbMasterWallet}</div>
              <div className="wb-master-amount">
                {/* Master Wallet is ALWAYS valued in Riyal (﷼), regardless of the SAR/Points toggle */}
                <RiyalMark size={22} currency="SAR" />
                <strong>{fmtTotal(masterTotal)}</strong>
              </div>
            </div>
            {canTransferMaster && (
              <button className="wb-rotate-btn" title={t.wbTransfer} onClick={() => openDrawer({
                id: selectedNode?.id, kind: 'org', name: selectedNode?.name, parentName: t.wbMasterWallet, isMaster: true,
              })}>
                <TransferIcon size={18} />
              </button>
            )}
          </div>

          {/* Per-channel sub-balances appear only in Multiple Wallets mode;
              in Single Wallet the Master Wallet shows just the one total. */}
          {role === 'falcon-admin' && walletType !== 'single' && <div className="wb-master-divider" />}

          {/* Falcon System Admin sees per-channel sub-balances (Multiple Wallets only) */}
          {role === 'falcon-admin' && walletType !== 'single' && (
            <div className="wb-master-subs">
              {/* Only the channels currently enabled in the table header (activeChannels)
                  are shown here — so the Master Wallet channel pools show/hide in
                  lock-step with the table's channel check-boxes. */}
              {WB_CHANNELS.filter(ch => activeChannels.includes(ch.id)).map(ch => {
                const sub = {
                  id: ch.id,
                  label: t[ch.label] || ({ whatsapp: 'WhatsApp', voice: 'Voice', aichat: 'AI-ChatGPT', sms: 'SMS', email: 'Email' })[ch.id] || ch.id,
                };
                const value = masterSubBalances[sub.id] || '0';
                return (
                  <div key={sub.id} className="wb-master-sub">
                    <div className="wb-master-sub-label">{sub.label}</div>
                    <div className="wb-master-sub-row">
                      <div className="wb-master-sub-field">
                        {/* Per-channel sub-balances follow the Currency toggle:
                            ﷼ when SAR, the Points coin when Points. */}
                        <RiyalMark size={16} currency={currency} />
                        {/* View only — wallet values are not editable */}
                        <span className="wb-master-sub-input wb-master-sub-input--readonly">{fmtNum(value)}</span>
                      </div>
                      {canTransferMaster && (
                        <button
                          className="wb-rotate-btn wb-rotate-btn-sub"
                          title={t.wbTransfer}
                          onClick={() => openDrawer({
                            id: `__ch_${sub.id}`,
                            ch: sub.id,
                            kind: 'commch',
                            isCommch: true,
                            name: `${t.wbMasterWallet} · ${sub.label}`,
                            parentName: t.wbMasterWallet,
                          })}
                        >
                          <TransferIcon size={16} />
                        </button>
                      )}
                    </div>
                  </div>
                );
              })}
            </div>
          )}
        </div>

        {/* Currency — moved DIRECTLY UNDER the Master Wallet box (per request);
            shown only AFTER Save (Balance/Wallet Type below lock to view once saved). */}
        {wbSaved && (
          <div className="wb-group">
            <div className="wb-group-sec">
              <div className="wb-group-label">{t.wbCurrency}</div>
              <div className="wb-radio-row">
                <RadioPill checked={currency === 'SAR'}    onChange={() => setCurrency('SAR')}    label={t.wbCurrencySAR} />
                <RadioPill checked={currency === 'Points'} onChange={() => setCurrency('Points')} label={t.wbCurrencyPoints} />
              </div>
              {/* Rate-card comm-channel picker: shown only in Single Wallet mode
                  (one pool → pick which channel's rate card drives the points
                  conversion). Hidden for Multiple Wallets (per request). */}
              {currency === 'Points' && walletType === 'single' && (
                <div className="wb-rate-channel">
                  <div className="wb-rate-channel-help">{t.wbRateCardChannel}</div>
                  <div className="wb-rate-channel-select-wrap">
                    <select
                      className="wb-rate-channel-select"
                      value={rateChannel}
                      onChange={(e) => setRateChannel(e.target.value)}
                    >
                      {WB_CHANNELS.map(ch => (
                        <option key={ch.id} value={ch.id}>{t[ch.label]}</option>
                      ))}
                    </select>
                    <span className="wb-rate-channel-caret" aria-hidden="true">
                      <IcChevronRight size={12} stroke={2.4} />
                    </span>
                  </div>
                </div>
              )}
            </div>
          </div>
        )}

        {/* Combined settings card — Rate Card commchannel,
            Balance Type, Wallet Type — all stacked inside ONE card divided
            by thin horizontal lines (matches the design spec). */}
        <div className="wb-group wb-group-stack">
          {/* Balance Type — after Save it's view-only: no radios, just the chosen value */}
          <div className="wb-group-sec">
            <div className="wb-group-label">{t.wbBalanceType}</div>
            {wbSaved ? (
              <div className="wb-view-value">{balanceType === 'node' ? t.wbNodeBased : t.wbUserBased}</div>
            ) : (
              <div className="wb-radio-stack">
                <RadioPill checked={balanceType === 'node'} onChange={() => setBalanceType('node')} label={t.wbNodeBased} />
                <RadioPill checked={balanceType === 'user'} onChange={() => setBalanceType('user')} label={t.wbUserBased} />
              </div>
            )}
          </div>

          <div className="wb-group-divider" />

          {/* Wallet Type — channel selection happens in the table header
              (per-column checkbox). A "Show All" link sits next to the
              Multiple Wallets radio so users can restore every channel
              after they've unchecked some from the header. */}
          <div className="wb-group-sec">
            <div className="wb-group-label">{t.wbWalletType}</div>
            {wbSaved ? (
              <div className="wb-mw-row">
                <div className="wb-view-value">{walletType === 'single' ? t.wbSingleWallet : t.wbMultipleWallets}</div>
                {walletType === 'multiple' && activeChannels.length < WB_CHANNELS.length && (
                  <button
                    type="button"
                    className="wb-show-all-link"
                    onClick={() => setActiveChannels(WB_CHANNELS.map(c => c.id))}
                  >
                    {t.wbShowAll || 'Show All'}
                  </button>
                )}
              </div>
            ) : (
              <div className="wb-radio-stack">
                <RadioPill checked={walletType === 'single'}   onChange={() => setWalletType('single')}   label={t.wbSingleWallet}   />
                <div className="wb-mw-row">
                  <RadioPill checked={walletType === 'multiple'} onChange={() => setWalletType('multiple')} label={t.wbMultipleWallets} />
                  {walletType === 'multiple' && activeChannels.length < WB_CHANNELS.length && (
                    <button
                      type="button"
                      className="wb-show-all-link"
                      onClick={() => setActiveChannels(WB_CHANNELS.map(c => c.id))}
                    >
                      {t.wbShowAll || 'Show All'}
                    </button>
                  )}
                </div>
              </div>
            )}
          </div>
        </div>

      </section>

      {/* ============== LEFT (via flex order): Allocation table ============== */}
      <section className="wb-table-col">
        {/* (Viewing-as + Switch perspective + Save moved into the top bar
            above so the page has a single header row that spans both columns) */}

        {/* Two-card split: Organizations on the left, Wallet / Transfer on
            the right. Both cards iterate the SAME tableRows array so any
            chevron expand/collapse updates both lockstep. Their inner scroll
            wraps are sync'd via JS (see useEffectW below) so dragging the
            scrollbar on either card moves the other. */}
        <div ref={wbSplitRef} className={`wb-table-split ${!canTransferRows ? 'wb-no-xfer' : ''} ${wbResizing ? 'is-resizing' : ''}`} data-cols={colsKey}>

          {/* ====== LEFT: Organizations card ====== */}
          <div className="wb-table-card wb-table-card-org" style={orgPaneW ? { flex: `0 0 ${orgPaneW}px` } : undefined}>
            {/* Header sits OUTSIDE the scroll so it stays fixed at the top and the
                scrollbar spans ONLY the rows (like the Show as Client table). */}
            <div className="wb-table-head-org">
              <div className="wb-th wb-th-org">{t.wbOrganizations}</div>
            </div>
            <div className="wb-table-scroll" data-scroll-sync="org" onWheel={onOrgWheel}>
              <div className="wb-table-stack" ref={wbOrgStackRef}>
              <div className="wb-table-body">
                {tableRows.map((row, rowIdx) => {
                  const isHeader = !!row.isHeader;
                  return (
                    <div key={row.id}
                      className={`wb-tr-org-card ${isHeader ? 'wb-tr-header' : 'wb-tr-child'} ${row.kind === 'user' ? 'wb-tr-user' : ''} ${wbHoveredPath && wbHoveredPath.has(row.id) ? 'on-path' : ''} ${wbHoveredRow === row.id ? 'wb-row-hover' : ''}`}
                      onMouseEnter={() => { if (wbScrollingRef.current) return; setWbHoveredPath(new Set(row.path)); setWbHoveredRow(row.id); }}
                      onMouseLeave={() => { setWbHoveredPath(null); setWbHoveredRow(null); }}>
                      <div className="wb-td wb-td-org">
                        {wbTreeRails(row, wbTrace, rowIdx, row.hasChildren && !!rowOpen[row.id])}
                        {row.kind === 'user' ? (
                          <span className="wb-td-chev wb-td-chev-spacer" />
                        ) : (
                          <button
                            className={`wb-td-chev ${row.hasChildren ? '' : 'invisible'} ${rowOpen[row.id] ? 'open' : ''}`}
                            onClick={() => row.hasChildren && toggleRow(row.id)}>
                            <IcChevronRight size={12} stroke={2.4} />
                          </button>
                        )}
                        {row.kind === 'user' && <UserDot name={row.name} />}
                        <span className="wb-td-name">{row.name}</span>
                      </div>
                    </div>
                  );
                })}
              </div>
              </div>{/* /.wb-table-stack */}
            </div>
          </div>

          {/* ====== Draggable resizer (Organizations ⟷ Wallet) ====== */}
          <div
            ref={wbResizerRef}
            className={`wb-split-resizer ${wbResizing ? 'dragging' : ''}`}
            role="separator"
            aria-orientation="vertical"
            aria-label={t.wbResizeColumns || 'Resize columns'}
            tabIndex={0}
            title={t.wbResizeHint || 'Drag to resize · double-click to reset'}
            onMouseDown={startOrgResize}
            onTouchStart={startOrgResize}
            onDoubleClick={() => setOrgPaneW(null)}
            onKeyDown={onOrgResizeKey}>
            <span ref={wbGripRef} className="wb-split-grip" aria-hidden="true">
              <span className="wb-split-grip-arrows" aria-hidden="true" />
            </span>
          </div>

          {/* ====== RIGHT: Wallet + Transfer card ====== */}
          <div className="wb-table-card wb-table-card-values">
            <div className="wb-table-scroll" data-scroll-sync="values" ref={wbValScrollRef} onScroll={onWalletScroll}>
              <div className="wb-table-stack">
              {/* Header INSIDE the stack (sticky): shares the rows' max-content width so
                  its channel columns line up with the value columns and scroll
                  horizontally together; sticky-top pins it during vertical scroll. */}
              <div className="wb-table-head-values" data-cols={colsKey}>
              {walletType === 'single' ? (
                <div className="wb-th wb-th-amt">{t.wbWallet}</div>
              ) : (
                activeChannels.map(chId => {
                  const ch = WB_CHANNELS.find(c => c.id === chId);
                  const isLast = activeChannels.length === 1;
                  return (
                    <label key={chId}
                      className={`wb-th wb-th-amt wb-th-channel wb-th-check ${isLast ? 'is-locked' : ''}`}
                      style={{ '--ch': ch.tone }}
                      title={isLast ? (t.wbAtLeastOneChannel || 'At least one channel must remain selected') : ''}>
                      <span className="wb-th-name">{t[ch.label]}</span>
                      <input
                        type="checkbox"
                        className="wb-th-check-input"
                        checked={true}
                        disabled={isLast}
                        onChange={() => !isLast && toggleChannel(ch.id)}
                      />
                      <span className="wb-th-check-box" aria-hidden="true">
                        <svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
                          <path d="m3.5 8.5 3 3 6-7" />
                        </svg>
                      </span>
                    </label>
                  );
                })
              )}
              <div className="wb-th wb-th-xfer" style={!canTransferRows ? { display: 'none' } : undefined}>{t.wbTransfer}</div>
              </div>{/* /.wb-table-head-values (sticky, inside stack) */}
              <div className="wb-table-body">
                {tableRows.map(row => {
                  const isHeader = !!row.isHeader;
                  return (
                    <div key={row.id}
                      className={`wb-tr-values-card ${isHeader ? 'wb-tr-header' : 'wb-tr-child'} ${row.kind === 'user' ? 'wb-tr-user' : ''} ${wbHoveredRow === row.id ? 'wb-row-hover' : ''}`}
                      data-cols={colsKey}
                      onMouseEnter={() => { if (wbScrollingRef.current) return; setWbHoveredPath(new Set(row.path)); setWbHoveredRow(row.id); }}
                      onMouseLeave={() => { setWbHoveredPath(null); setWbHoveredRow(null); }}>
                      {/* In User-Based mode the value input belongs to the
                          USER, not the parent org. Org rows render an empty
                          placeholder cell so the grid column still exists but
                          no input box is shown. */}
                      {(() => {
                        const hideValue = balanceType === 'user' && row.kind !== 'user';
                        if (walletType === 'single') {
                          return (
                            <div className="wb-td wb-td-amt">
                              {!hideValue && (
                                /* View only — Wallet values are never editable */
                                <span className="wb-amount-view">
                                  <RiyalMark size={13} currency={currency} />
                                  <strong>{fmtNum(getAlloc(row, 'single'))}</strong>
                                </span>
                              )}
                            </div>
                          );
                        }
                        return activeChannels.map(chId => (
                          <div key={chId} className="wb-td wb-td-amt">
                            {!hideValue && (
                              /* View only — Wallet values are never editable */
                              <span className="wb-amount-view">
                                <RiyalMark size={13} currency={currency} />
                                <strong>{fmtNum(getAlloc(row, chId))}</strong>
                              </span>
                            )}
                          </div>
                        ));
                      })()}

                      {canTransferRows && (
                        <div className="wb-td wb-td-xfer">
                          {(balanceType !== 'user' || row.kind === 'user') && (
                            <button className="wb-xfer-btn" onClick={() => openDrawer(row)} title={t.wbTransfer}>
                              <TransferIcon size={15} />
                            </button>
                          )}
                        </div>
                      )}
                    </div>
                  );
                })}
              </div>
              </div>{/* /.wb-table-stack */}
            </div>
          </div>

        </div>
      </section>

      </div>{/* /.wb-body-row */}
        </div>
      </div>

      {/* ============== Slide-in: Balance Transfer drawer ============== */}
      {drawerFor && (
        <BalanceTransferDrawer
          source={drawerFor}
          rows={tableRows}
          masterName={selectedNode?.name}
          channels={walletType === 'multiple' ? activeChannels : null}
          walletType={walletType}
          balanceType={balanceType}
          masterSubBalances={masterSubBalances}
          getAlloc={getAlloc}
          t={t}
          onClose={() => setDrawerFor(null)}
          onConfirm={onConfirmTransfer}
        />
      )}

      {/* ============== Confirm-save modal ============== */}
      {confirmSaveOpen && (
        <div
          className="ac-modal-overlay wb-confirm-overlay"
          role="dialog"
          aria-modal="true"
          aria-labelledby="wb-confirm-title"
          aria-describedby="wb-confirm-sub"
          onClick={cancelSave}
        >
          <div className="ac-modal wb-confirm-modal" onClick={(e) => e.stopPropagation()}>
            <button
              type="button"
              className="ac-modal-close"
              aria-label={t.cancel || 'Cancel'}
              onClick={cancelSave}
            >
              <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M6 6L18 18M18 6L6 18" /></svg>
            </button>
            <div className="wb-confirm-icon" aria-hidden="true">
              {/* Save & lock badge — a padlock with a checkmark, in the brand
                  teal (same as the Save button) on a soft cyan halo. Conveys
                  "your edit is saved and the allocation is locked" — far better
                  suited to this positive action than a red danger triangle. */}
              <svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
                <defs>
                  <linearGradient id="wbLockGrad" x1="40" y1="13" x2="40" y2="67" gradientUnits="userSpaceOnUse">
                    <stop stopColor="#15565C" />
                    <stop offset="1" stopColor="#0A3338" />
                  </linearGradient>
                  <radialGradient id="wbLockHalo" cx="40" cy="38" r="40" gradientUnits="userSpaceOnUse">
                    <stop offset="0.5" stopColor="#2DD4D9" stopOpacity="0.22" />
                    <stop offset="1" stopColor="#2DD4D9" stopOpacity="0" />
                  </radialGradient>
                </defs>
                {/* soft glow + tint ring + brand badge */}
                <circle cx="40" cy="40" r="38" fill="url(#wbLockHalo)" />
                <circle cx="40" cy="40" r="30" fill="#E8F0F1" />
                <circle cx="40" cy="40" r="26" fill="url(#wbLockGrad)" />
                <ellipse cx="40" cy="30" rx="17" ry="9" fill="#ffffff" opacity="0.07" />
                {/* padlock shackle + body */}
                <path d="M32 38 V33 a8 8 0 0 1 16 0 V38" stroke="#ffffff" strokeWidth="3.4" fill="none" strokeLinecap="round" />
                <rect x="27.5" y="37.5" width="25" height="19" rx="5" fill="#ffffff" />
                {/* checkmark (animated draw) */}
                <path className="wb-lock-check" d="M34.8 47.2 l3.4 3.4 l6.8 -7.4" stroke="#0D3F44" strokeWidth="3" fill="none" strokeLinecap="round" strokeLinejoin="round" />
              </svg>
            </div>
            <h3 id="wb-confirm-title" className="wb-confirm-title">{t.wbConfirmSaveTitle || 'Are you sure you want to save the edit?'}</h3>
            <p id="wb-confirm-sub" className="wb-confirm-sub">{t.wbConfirmSaveSub || 'Once you save, the wallet allocation for this client will be locked until you leave and return to the page.'}</p>
            <div className="wb-confirm-actions">
              <button type="button" className="btn btn-secondary" onClick={cancelSave} autoFocus>
                {t.cancel || 'Cancel'}
              </button>
              <button type="button" className="btn btn-primary" onClick={confirmSave}>
                {t.save || 'Save'}
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
};

// ====== Reusable radio pill ======
const RadioPill = ({ checked, onChange, label, helper, disabled }) => (
  <label className={`wb-radio ${checked ? 'checked' : ''} ${disabled ? 'wb-radio--view' : ''}`} onClick={disabled ? undefined : onChange}>
    <span className={`wb-radio-circle ${checked ? 'checked' : ''}`}>
      {checked && <span className="wb-radio-dot" />}
    </span>
    <span className="wb-radio-label-wrap">
      <span className="wb-radio-label">{label}</span>
      {helper && <span className="wb-radio-helper">{helper}</span>}
    </span>
  </label>
);

// ====== Multi-select dropdown for active channels (opens UPWARD) ======
// Replaces the previous wb-ch-toggle pill row. Selected channels render as
// chips inside the trigger; the menu drops UP (bottom-anchored) so it doesn't
// push the table down when opened. Last remaining channel can't be removed.
const WbChannelsMultiSelect = ({ channels, value, onToggle, t }) => {
  const [open, setOpen] = useStateW(false);
  const wrap = useRefW(null);
  useEffectW(() => {
    if (!open) return;
    const onDoc = (e) => { if (wrap.current && !wrap.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [open]);
  const selected = channels.filter(c => value.includes(c.id));
  const isLast = (id) => value.length === 1 && value.includes(id);
  return (
    <div className={`wb-chsel ${open ? 'is-open' : ''}`} ref={wrap}>
      <button type="button" className="wb-chsel-trigger" onClick={() => setOpen(o => !o)}>
        <span className="wb-chsel-chips">
          {selected.length === 0 ? (
            <span className="wb-chsel-placeholder">{t.wbSelect || 'Select…'}</span>
          ) : (
            selected.map(ch => (
              <span key={ch.id} className="wb-chsel-chip">{t[ch.label]}</span>
            ))
          )}
        </span>
        <span className="wb-chsel-caret" aria-hidden="true">
          <IcChevronRight size={12} stroke={2.4} />
        </span>
      </button>
      {open && (
        <div className="wb-chsel-menu" role="listbox" aria-multiselectable="true">
          {channels.map(ch => {
            const checked = value.includes(ch.id);
            const locked = isLast(ch.id);
            return (
              <label key={ch.id}
                className={`wb-chsel-opt ${checked ? 'is-checked' : ''} ${locked ? 'is-locked' : ''}`}
                title={locked ? (t.wbAtLeastOneChannel || 'At least one channel must remain selected') : ''}>
                <input
                  type="checkbox"
                  checked={checked}
                  disabled={locked}
                  onChange={() => !locked && onToggle(ch.id)}
                />
                <span className="wb-chsel-opt-box" aria-hidden="true">
                  {checked && (
                    <svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
                      <path d="m3.5 8.5 3 3 6-7" />
                    </svg>
                  )}
                </span>
                <span className="wb-chsel-opt-label">{t[ch.label]}</span>
              </label>
            );
          })}
        </div>
      )}
    </div>
  );
};

window.WalletPage = WalletPage;
