// Data Room v2 — full source-data viewer.
//
// One landing index per tenant showing every canonical table that has rows,
// plus a generic server-side grid (search / filter / sort / paginate / CSV
// export) for any one of them. Every query is RLS-scoped and pushed to
// Supabase — the browser never holds more than one page at a time.

const { useState: useDR2State, useEffect: useDR2Effect, useMemo: useDR2Memo, useCallback: useDR2Cb, useRef: useDR2Ref } = React;

// ── Canonical table registry ────────────────────────────────────────────
// Single source of truth for what the Data Room knows about. Order = display
// order inside each group. A table is shown only if it has > 0 rows for the
// current tenant — the row-count probe runs on the index page.
//
// Per table:
//   key             Supabase table name (also URL key)
//   label / desc    UI strings
//   group           Section in the index
//   dateCol         Date column for month/range filters (null = reference table)
//   amountCol       Optional numeric col for "amount this period" preview
//   searchCols      Cols included in the text-search OR filter (ilike)
//   filters         Per-column filter widgets (select w/ distinct values)
//   columns         Grid columns
//   defaultSort     Initial sort
const DR_TABLES = [
  // ── Accounting ─────────────────────────────────────────────
  {
    key: 'gl_transactions',
    label: 'GL Transactions',
    desc: 'Every general-ledger line, with JE, account, doc, party, memo.',
    group: 'Accounting',
    dateCol: 'posted_date',
    amountCol: 'amount',
    searchCols: ['account_code','account_name','memo','reference','party_name','doc_no','je_id'],
    filters: [
      { col: 'account_code', label: 'Account',  source: 'distinct', display: 'code' },
      { col: 'doc_type',     label: 'Doc type', source: 'distinct' },
      { col: 'class_dept',   label: 'Class / dept', source: 'distinct' },
      { col: 'source_system',label: 'Source',   source: 'distinct' },
    ],
    columns: [
      { key: 'posted_date',  label: 'Date',   type: 'date',  w: 96 },
      { key: 'je_id',        label: 'JE',     type: 'mono',  w: 80 },
      { key: 'doc_type',     label: 'Doc',    type: 'chip',  w: 76 },
      { key: 'doc_no',       label: 'Doc #',  type: 'mono',  w: 90 },
      { key: 'account_code', label: 'Account', type: 'mono', w: 90 },
      { key: 'account_name', label: 'Name' },
      { key: 'party_name',   label: 'Party' },
      { key: 'memo',         label: 'Memo',   ellipsis: 280 },
      { key: 'debit',        label: 'Debit',  type: 'num',  align: 'right', w: 100 },
      { key: 'credit',       label: 'Credit', type: 'num',  align: 'right', w: 100 },
      { key: 'amount',       label: 'Amount', type: 'num',  align: 'right', w: 110, signed: true },
      { key: 'class_dept',   label: 'Class' },
    ],
    defaultSort: { col: 'posted_date', dir: 'desc' },
  },
  {
    key: 'account_master',
    label: 'Chart of Accounts',
    desc: 'GL account master — type, normal balance, statement.',
    group: 'Accounting',
    dateCol: null,
    searchCols: ['account_no','account_name','account_type','statement'],
    filters: [
      { col: 'account_type',   label: 'Type',    source: 'distinct' },
      { col: 'statement',      label: 'Statement', source: 'distinct' },
      { col: 'normal_balance', label: 'Normal',  source: 'distinct' },
    ],
    columns: [
      { key: 'account_no',     label: 'Code',     type: 'mono', w: 90 },
      { key: 'account_name',   label: 'Name' },
      { key: 'account_type',   label: 'Type',     type: 'chip' },
      { key: 'normal_balance', label: 'Normal',   w: 70 },
      { key: 'statement',      label: 'Statement', w: 110 },
      { key: 'is_control',     label: 'Control',  type: 'bool', w: 80 },
      { key: 'source',         label: 'Source',   w: 90 },
    ],
    defaultSort: { col: 'account_no', dir: 'asc' },
  },
  {
    key: 'account_mappings',
    label: 'Account Mappings',
    desc: 'Each GL account mapped to a canonical category / subcategory.',
    group: 'Accounting',
    dateCol: null,
    searchCols: ['account_code','account_name','canonical_category','subcategory'],
    filters: [
      { col: 'canonical_category', label: 'Category',   source: 'distinct' },
      { col: 'subcategory',        label: 'Subcategory', source: 'distinct' },
    ],
    columns: [
      { key: 'account_code',       label: 'Code',     type: 'mono', w: 90 },
      { key: 'account_name',       label: 'Name' },
      { key: 'canonical_category', label: 'Category', type: 'chip' },
      { key: 'subcategory',        label: 'Subcategory' },
      { key: 'confidence',         label: 'Conf',     type: 'num', align: 'right', w: 70 },
      { key: 'source',             label: 'Source',   w: 90 },
    ],
    defaultSort: { col: 'account_code', dir: 'asc' },
  },

  // ── Receivables ────────────────────────────────────────────
  {
    key: 'ar_invoices',
    label: 'AR Invoices',
    desc: 'Customer invoices — issued, due, paid, balance.',
    group: 'Receivables',
    dateCol: 'issue_date',
    amountCol: 'total',
    searchCols: ['invoice_no','customer_name','customer_id','sales_rep','terms'],
    filters: [
      { col: 'status',     label: 'Status',     source: 'distinct' },
      { col: 'sales_rep',  label: 'Sales rep',  source: 'distinct' },
      { col: 'terms',      label: 'Terms',      source: 'distinct' },
    ],
    columns: [
      { key: 'invoice_no',    label: 'Invoice #', type: 'mono', w: 110 },
      { key: 'issue_date',    label: 'Issued',    type: 'date', w: 96 },
      { key: 'due_date',      label: 'Due',       type: 'date', w: 96 },
      { key: 'customer_name', label: 'Customer' },
      { key: 'terms',         label: 'Terms',     w: 80 },
      { key: 'subtotal',      label: 'Subtotal',  type: 'num', align: 'right', w: 100 },
      { key: 'tax',           label: 'Tax',       type: 'num', align: 'right', w: 80 },
      { key: 'total',         label: 'Total',     type: 'num', align: 'right', w: 110 },
      { key: 'paid_amount',   label: 'Paid',      type: 'num', align: 'right', w: 100 },
      { key: 'balance',       label: 'Balance',   type: 'num', align: 'right', w: 110 },
      { key: 'status',        label: 'Status',    type: 'chip' },
    ],
    defaultSort: { col: 'issue_date', dir: 'desc' },
  },
  {
    key: 'ar_aging_snapshot',
    label: 'AR Aging Snapshot',
    desc: 'Customer aging bucketed by current / 30 / 60 / 90 / 90+.',
    group: 'Receivables',
    dateCol: 'as_of_date',
    amountCol: 'total',
    searchCols: ['customer_name'],
    filters: [],
    columns: [
      { key: 'as_of_date',     label: 'As of',     type: 'date', w: 96 },
      { key: 'customer_name',  label: 'Customer' },
      { key: 'bucket_current', label: 'Current', type: 'num', align: 'right' },
      { key: 'bucket_1_30',    label: '1–30',    type: 'num', align: 'right' },
      { key: 'bucket_31_60',   label: '31–60',   type: 'num', align: 'right' },
      { key: 'bucket_61_90',   label: '61–90',   type: 'num', align: 'right' },
      { key: 'bucket_90_plus', label: '90+',     type: 'num', align: 'right' },
      { key: 'total',          label: 'Total',   type: 'num', align: 'right' },
    ],
    defaultSort: { col: 'as_of_date', dir: 'desc' },
  },
  {
    key: 'customers_master',
    label: 'Customers',
    desc: 'Customer master — lifetime value, orders, region, terms.',
    group: 'Receivables',
    dateCol: null,
    searchCols: ['name','email','segment','acquisition_channel','region','contact','external_id'],
    filters: [
      { col: 'segment',  label: 'Segment',  source: 'distinct' },
      { col: 'status',   label: 'Status',   source: 'distinct' },
      { col: 'region',   label: 'Region',   source: 'distinct' },
      { col: 'acquisition_channel', label: 'Channel', source: 'distinct' },
    ],
    columns: [
      { key: 'name',                label: 'Name' },
      { key: 'email',               label: 'Email' },
      { key: 'segment',             label: 'Segment',  type: 'chip' },
      { key: 'region',              label: 'Region' },
      { key: 'acquisition_channel', label: 'Channel' },
      { key: 'acquisition_date',    label: 'Acquired', type: 'date',  w: 96 },
      { key: 'order_count',         label: 'Orders',   type: 'num',   align: 'right', w: 80 },
      { key: 'lifetime_value_cents', label: 'LTV',     type: 'cents', align: 'right', w: 110 },
      { key: 'last_order_date',     label: 'Last order', type: 'date', w: 96 },
      { key: 'status',              label: 'Status' },
    ],
    defaultSort: { col: 'lifetime_value_cents', dir: 'desc' },
  },

  // ── Payables ───────────────────────────────────────────────
  {
    key: 'ap_bills',
    label: 'AP Bills',
    desc: 'Vendor bills — issued, due, open balance.',
    group: 'Payables',
    dateCol: 'issue_date',
    amountCol: 'amount',
    searchCols: ['bill_no','vendor_name','po_no','memo','category','terms'],
    filters: [
      { col: 'vendor_name', label: 'Vendor',   source: 'distinct' },
      { col: 'category',    label: 'Category', source: 'distinct' },
      { col: 'terms',       label: 'Terms',    source: 'distinct' },
    ],
    columns: [
      { key: 'bill_no',      label: 'Bill #',  type: 'mono', w: 110 },
      { key: 'issue_date',   label: 'Issued',  type: 'date', w: 96 },
      { key: 'due_date',     label: 'Due',     type: 'date', w: 96 },
      { key: 'vendor_name',  label: 'Vendor' },
      { key: 'category',     label: 'Category', type: 'chip' },
      { key: 'po_no',        label: 'PO',      type: 'mono', w: 90 },
      { key: 'amount',       label: 'Amount',  type: 'num', align: 'right', w: 110 },
      { key: 'open_balance', label: 'Open',    type: 'num', align: 'right', w: 110 },
      { key: 'terms',        label: 'Terms',   w: 80 },
      { key: 'memo',         label: 'Memo',    ellipsis: 240 },
    ],
    defaultSort: { col: 'issue_date', dir: 'desc' },
  },
  {
    key: 'ap_aging_snapshot',
    label: 'AP Aging Snapshot',
    desc: 'Vendor aging bucketed by current / 30 / 60 / 90 / 90+.',
    group: 'Payables',
    dateCol: 'as_of_date',
    amountCol: 'total',
    searchCols: ['vendor_name'],
    filters: [],
    columns: [
      { key: 'as_of_date',     label: 'As of',     type: 'date', w: 96 },
      { key: 'vendor_name',    label: 'Vendor' },
      { key: 'bucket_current', label: 'Current', type: 'num', align: 'right' },
      { key: 'bucket_1_30',    label: '1–30',    type: 'num', align: 'right' },
      { key: 'bucket_31_60',   label: '31–60',   type: 'num', align: 'right' },
      { key: 'bucket_61_90',   label: '61–90',   type: 'num', align: 'right' },
      { key: 'bucket_90_plus', label: '90+',     type: 'num', align: 'right' },
      { key: 'total',          label: 'Total',   type: 'num', align: 'right' },
    ],
    defaultSort: { col: 'as_of_date', dir: 'desc' },
  },
  {
    key: 'vendors_master',
    label: 'Vendors',
    desc: 'Vendor master — category, terms, 1099 flag, contact.',
    group: 'Payables',
    dateCol: null,
    searchCols: ['name','vendor_id','category','contact','terms'],
    filters: [
      { col: 'category', label: 'Category', source: 'distinct' },
      { col: 'status',   label: 'Status',   source: 'distinct' },
      { col: 'terms',    label: 'Terms',    source: 'distinct' },
    ],
    columns: [
      { key: 'name',     label: 'Name' },
      { key: 'vendor_id', label: 'Vendor ID', type: 'mono', w: 100 },
      { key: 'category', label: 'Category', type: 'chip' },
      { key: 'terms',    label: 'Terms',    w: 80 },
      { key: 'is_1099',  label: '1099',     type: 'bool', w: 60 },
      { key: 'contact',  label: 'Contact' },
      { key: 'phone',    label: 'Phone' },
      { key: 'status',   label: 'Status' },
    ],
    defaultSort: { col: 'name', dir: 'asc' },
  },

  // ── Payroll / HR ───────────────────────────────────────────
  {
    key: 'employees',
    label: 'Employees',
    desc: 'Employee master — department, title, pay type, salary.',
    group: 'Payroll',
    dateCol: null,
    searchCols: ['name','employee_id','department','title','location'],
    filters: [
      { col: 'department',      label: 'Department', source: 'distinct' },
      { col: 'employment_type', label: 'Employment', source: 'distinct' },
      { col: 'pay_type',        label: 'Pay type',   source: 'distinct' },
      { col: 'status',          label: 'Status',     source: 'distinct' },
      { col: 'location',        label: 'Location',   source: 'distinct' },
    ],
    columns: [
      { key: 'employee_id',     label: 'ID',         type: 'mono', w: 80 },
      { key: 'name',            label: 'Name' },
      { key: 'department',      label: 'Dept',       type: 'chip' },
      { key: 'title',           label: 'Title' },
      { key: 'employment_type', label: 'Type',       w: 90 },
      { key: 'pay_type',        label: 'Pay',        w: 80 },
      { key: 'hire_date',       label: 'Hired',      type: 'date', w: 96 },
      { key: 'annual_salary',   label: 'Salary',     type: 'num', align: 'right', w: 110 },
      { key: 'location',        label: 'Location' },
      { key: 'status',          label: 'Status' },
    ],
    defaultSort: { col: 'name', dir: 'asc' },
  },
  {
    key: 'payroll_runs',
    label: 'Payroll Register',
    desc: 'Employee × pay-period — gross, taxes, benefits, net.',
    group: 'Payroll',
    dateCol: 'pay_date',
    amountCol: 'gross_pay',
    searchCols: ['employee_name','employee_id','department','title','pay_period'],
    filters: [
      { col: 'department', label: 'Department', source: 'distinct' },
      { col: 'pay_period', label: 'Pay period', source: 'distinct' },
    ],
    columns: [
      { key: 'pay_date',      label: 'Pay date', type: 'date', w: 96 },
      { key: 'pay_period',    label: 'Period',   type: 'mono', w: 110 },
      { key: 'employee_id',   label: 'EID',      type: 'mono', w: 70 },
      { key: 'employee_name', label: 'Employee' },
      { key: 'department',    label: 'Dept',     type: 'chip' },
      { key: 'title',         label: 'Title' },
      { key: 'gross_pay',     label: 'Gross',    type: 'num', align: 'right', w: 100 },
      { key: 'fed_tax',       label: 'Fed',      type: 'num', align: 'right', w: 90 },
      { key: 'state_tax',     label: 'State',    type: 'num', align: 'right', w: 90 },
      { key: 'fica',          label: 'FICA',     type: 'num', align: 'right', w: 90 },
      { key: 'benefits',      label: 'Benefits', type: 'num', align: 'right', w: 100 },
      { key: 'net_pay',       label: 'Net',      type: 'num', align: 'right', w: 100 },
    ],
    defaultSort: { col: 'pay_date', dir: 'desc' },
  },

  // ── Sales & Products ───────────────────────────────────────
  {
    key: 'sales_orders',
    label: 'Sales Orders',
    desc: 'Order headers — customer, channel, totals, GP.',
    group: 'Sales',
    dateCol: 'order_date',
    amountCol: 'total_cents',
    searchCols: ['order_number','customer_name','channel','status'],
    filters: [
      { col: 'channel', label: 'Channel', source: 'distinct' },
      { col: 'status',  label: 'Status',  source: 'distinct' },
      { col: 'currency', label: 'Currency', source: 'distinct' },
    ],
    columns: [
      { key: 'order_date',         label: 'Date',     type: 'date', w: 96 },
      { key: 'order_number',       label: 'Order #',  type: 'mono', w: 110 },
      { key: 'customer_name',      label: 'Customer' },
      { key: 'channel',            label: 'Channel',  type: 'chip' },
      { key: 'status',             label: 'Status',   type: 'chip' },
      { key: 'currency',           label: 'Cur',      w: 60 },
      { key: 'subtotal_cents',     label: 'Subtotal',  type: 'cents', align: 'right', w: 100 },
      { key: 'discount_cents',     label: 'Discount',  type: 'cents', align: 'right', w: 100 },
      { key: 'tax_cents',          label: 'Tax',       type: 'cents', align: 'right', w: 90 },
      { key: 'total_cents',        label: 'Total',     type: 'cents', align: 'right', w: 110 },
      { key: 'cogs_cents',         label: 'COGS',      type: 'cents', align: 'right', w: 100 },
      { key: 'gross_profit_cents', label: 'GP',        type: 'cents', align: 'right', w: 100, signed: true },
    ],
    defaultSort: { col: 'order_date', dir: 'desc' },
  },
  {
    key: 'sales_line_items',
    label: 'Sales Line Items',
    desc: 'Line-level detail — SKU, qty, price, margin.',
    group: 'Sales',
    dateCol: 'order_date',
    amountCol: 'line_total_cents',
    searchCols: ['sku','product_name','category','sub_category','channel'],
    filters: [
      { col: 'category',    label: 'Category',    source: 'distinct' },
      { col: 'sub_category', label: 'Subcategory', source: 'distinct' },
      { col: 'channel',     label: 'Channel',     source: 'distinct' },
    ],
    columns: [
      { key: 'order_date',         label: 'Date',     type: 'date', w: 96 },
      { key: 'sku',                label: 'SKU',      type: 'mono', w: 100 },
      { key: 'product_name',       label: 'Product' },
      { key: 'category',           label: 'Category', type: 'chip' },
      { key: 'sub_category',       label: 'Subcat' },
      { key: 'channel',            label: 'Channel' },
      { key: 'quantity',           label: 'Qty',      type: 'num',   align: 'right', w: 70 },
      { key: 'unit_price_cents',   label: 'Unit $',   type: 'cents', align: 'right', w: 90 },
      { key: 'unit_cost_cents',    label: 'Unit cost', type: 'cents', align: 'right', w: 90 },
      { key: 'line_total_cents',   label: 'Total',    type: 'cents', align: 'right', w: 110 },
      { key: 'line_cost_cents',    label: 'Cost',     type: 'cents', align: 'right', w: 100 },
      { key: 'line_gross_profit_cents', label: 'GP',  type: 'cents', align: 'right', w: 100, signed: true },
      { key: 'gross_margin_pct',   label: 'GM%',      type: 'pct',   align: 'right', w: 70 },
    ],
    defaultSort: { col: 'order_date', dir: 'desc' },
  },
  {
    key: 'products',
    label: 'Products / SKUs',
    desc: 'Product master — cost, price, on-hand, reorder point.',
    group: 'Sales',
    dateCol: null,
    searchCols: ['sku','name','category','sub_category','preferred_vendor'],
    filters: [
      { col: 'category',     label: 'Category',     source: 'distinct' },
      { col: 'sub_category', label: 'Subcategory',  source: 'distinct' },
      { col: 'is_active',    label: 'Active',       source: 'distinct' },
    ],
    columns: [
      { key: 'sku',               label: 'SKU',     type: 'mono', w: 100 },
      { key: 'name',              label: 'Name' },
      { key: 'category',          label: 'Category', type: 'chip' },
      { key: 'sub_category',      label: 'Subcat' },
      { key: 'uom',               label: 'UOM',     w: 60 },
      { key: 'unit_cost_cents',   label: 'Cost',    type: 'cents', align: 'right', w: 90 },
      { key: 'unit_price_cents',  label: 'Price',   type: 'cents', align: 'right', w: 90 },
      { key: 'ending_qty',        label: 'On hand', type: 'num',   align: 'right', w: 90 },
      { key: 'ending_value_cents', label: 'On-hand $', type: 'cents', align: 'right', w: 110 },
      { key: 'reorder_point',     label: 'Reorder', type: 'num',   align: 'right', w: 90 },
      { key: 'preferred_vendor',  label: 'Vendor' },
      { key: 'is_active',         label: 'Active',  type: 'bool',  w: 70 },
    ],
    defaultSort: { col: 'name', dir: 'asc' },
  },

  // ── SaaS ───────────────────────────────────────────────────
  {
    key: 'saas_subscriptions',
    label: 'Subscriptions',
    desc: 'Active and historical subscriptions with MRR / ARR.',
    group: 'SaaS',
    dateCol: 'started_at',
    amountCol: 'mrr_cents',
    searchCols: ['customer_name','plan_name','status','external_id'],
    filters: [
      { col: 'plan_name', label: 'Plan',   source: 'distinct' },
      { col: 'status',    label: 'Status', source: 'distinct' },
    ],
    columns: [
      { key: 'started_at',    label: 'Started',  type: 'date', w: 96 },
      { key: 'customer_name', label: 'Customer' },
      { key: 'plan_name',     label: 'Plan',     type: 'chip' },
      { key: 'status',        label: 'Status',   type: 'chip' },
      { key: 'mrr_cents',     label: 'MRR',      type: 'cents', align: 'right', w: 100 },
      { key: 'arr_cents',     label: 'ARR',      type: 'cents', align: 'right', w: 110 },
      { key: 'trial_ends_at', label: 'Trial ends', type: 'date', w: 96 },
      { key: 'cancelled_at',  label: 'Cancelled', type: 'date', w: 96 },
    ],
    defaultSort: { col: 'mrr_cents', dir: 'desc' },
  },
  {
    key: 'saas_mrr_events',
    label: 'MRR Events',
    desc: 'New / expansion / contraction / churn / reactivation events.',
    group: 'SaaS',
    dateCol: 'event_date',
    amountCol: 'mrr_delta_cents',
    searchCols: ['customer_name','plan_name','event_type'],
    filters: [
      { col: 'event_type', label: 'Event', source: 'distinct' },
      { col: 'plan_name',  label: 'Plan',  source: 'distinct' },
    ],
    columns: [
      { key: 'event_date',      label: 'Date',     type: 'date', w: 96 },
      { key: 'event_type',      label: 'Event',    type: 'chip' },
      { key: 'customer_name',   label: 'Customer' },
      { key: 'plan_name',       label: 'Plan' },
      { key: 'mrr_delta_cents', label: 'Δ MRR',    type: 'cents', align: 'right', w: 110, signed: true },
    ],
    defaultSort: { col: 'event_date', dir: 'desc' },
  },
];

const DR_TABLE_BY_KEY = Object.fromEntries(DR_TABLES.map(t => [t.key, t]));

// ── companyId resolver (mirrors v1) ─────────────────────────────────────
function dr2UseCompanyId(scopedCompanyId) {
  const [cid, setCid] = useDR2State(scopedCompanyId || null);
  useDR2Effect(() => {
    if (scopedCompanyId) { setCid(scopedCompanyId); return; }
    const db = window.supabaseClient;
    if (!db) return;
    (async () => {
      const { data: { session } } = await db.auth.getSession();
      if (!session) return;
      const { data: m } = await db.from('company_users')
        .select('company_id').eq('user_id', session.user.id)
        .in('status', ['Active', 'Pending']).maybeSingle();
      if (m?.company_id) setCid(m.company_id);
    })();
  }, [scopedCompanyId]);
  return cid;
}

// ── Shell wrapper (consistent header) ───────────────────────────────────
function DR2Shell({ title, subtitle, action, children, breadcrumb, onBack }) {
  return (
    <div className="pc-page">
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
        <div>
          <div style={{ fontSize: 11, color: 'var(--text-3)', textTransform: 'uppercase', letterSpacing: 0.5, display: 'flex', alignItems: 'center', gap: 6 }}>
            {onBack && (
              <button onClick={onBack} style={{ background: 'none', border: 0, padding: 0, color: 'var(--text-3)', cursor: 'pointer', fontSize: 11, textTransform: 'uppercase', letterSpacing: 0.5 }}>Data room</button>
            )}
            {!onBack && 'Data room'}
            {breadcrumb && <span style={{ color: 'var(--text-4)' }}>›</span>}
            {breadcrumb && <span style={{ color: 'var(--text-3)' }}>{breadcrumb}</span>}
          </div>
          <h2 style={{ margin: '2px 0 0', fontSize: 22, fontWeight: 600, letterSpacing: -0.3 }}>{title}</h2>
          {subtitle && <div style={{ fontSize: 13, color: 'var(--text-2)', marginTop: 6 }}>{subtitle}</div>}
        </div>
        <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>{action}</div>
      </div>
      {children}
    </div>
  );
}

// ── Index page — populated tables for this tenant ───────────────────────
function DR2IndexPage({ companyId, onOpen, companyProfile, onLeave }) {
  const [loading, setLoading] = useDR2State(true);
  const [counts, setCounts]   = useDR2State({});  // { tableKey: number }
  const [errors, setErrors]   = useDR2State({});

  useDR2Effect(() => {
    if (!companyId) { setLoading(false); return; }
    const db = window.supabaseClient;
    if (!db) { setLoading(false); return; }
    let cancelled = false;
    setLoading(true);
    (async () => {
      // Run all count probes in parallel. HEAD requests don't ship rows.
      const results = await Promise.all(DR_TABLES.map(async (t) => {
        try {
          const { count, error } = await db.from(t.key)
            .select('*', { count: 'exact', head: true })
            .eq('company_id', companyId);
          if (error) return [t.key, { error: error.message }];
          return [t.key, { count: count || 0 }];
        } catch (e) {
          return [t.key, { error: String(e) }];
        }
      }));
      if (cancelled) return;
      const nextCounts = {}; const nextErrs = {};
      for (const [k, v] of results) {
        if (v.error) nextErrs[k] = v.error; else nextCounts[k] = v.count;
      }
      setCounts(nextCounts);
      setErrors(nextErrs);
      setLoading(false);
    })();
    return () => { cancelled = true; };
  }, [companyId]);

  // Only show tables that actually have data.
  const populated = DR_TABLES.filter(t => (counts[t.key] || 0) > 0);
  const grouped   = useDR2Memo(() => {
    const g = {};
    populated.forEach(t => { (g[t.group] ||= []).push(t); });
    return g;
  }, [populated]);

  if (loading) {
    return (
      <DR2Shell title="Data Room" subtitle="Probing your tables…">
        <Card><div style={{ padding: 32, textAlign: 'center', color: 'var(--text-3)' }}>Counting rows…</div></Card>
      </DR2Shell>
    );
  }

  if (!populated.length) {
    return (
      <DR2Shell title="Data Room" subtitle="Every canonical table this tenant has data in.">
        <Card>
          <div style={{ padding: '48px 24px', textAlign: 'center' }}>
            <div style={{ fontSize: 16, fontWeight: 600, marginBottom: 6 }}>No source data yet</div>
            <div style={{ fontSize: 13, color: 'var(--text-2)', maxWidth: 460, margin: '0 auto', lineHeight: 1.55 }}>
              Once a sync (or workbook upload) populates a canonical table for this tenant, it will appear here automatically.
            </div>
          </div>
        </Card>
      </DR2Shell>
    );
  }

  const totalRows = populated.reduce((s, t) => s + (counts[t.key] || 0), 0);

  return (
    <DR2Shell
      title="Data Room"
      subtitle={`${populated.length} table${populated.length === 1 ? '' : 's'} populated · ${totalRows.toLocaleString()} total row${totalRows === 1 ? '' : 's'}${companyProfile?.name ? ' · ' + companyProfile.name : ''}`}
      action={onLeave && (
        <button onClick={onLeave} style={{ display: 'flex', alignItems: 'center', gap: 6, background: 'var(--bg-elev-2)', border: '1px solid var(--border)', borderRadius: 7, padding: '6px 14px', fontSize: 11, fontWeight: 700, color: 'var(--text-1)', cursor: 'pointer' }}>← Back</button>
      )}
    >
      {Object.entries(grouped).map(([group, tables]) => (
        <div key={group} style={{ marginTop: 18 }}>
          <div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-3)', textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 8 }}>{group}</div>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 10 }}>
            {tables.map(t => (
              <button key={t.key} onClick={() => onOpen(t.key)}
                      className="dr2-card"
                      style={{
                        textAlign: 'left', background: 'var(--bg-card)', border: '1px solid var(--border)',
                        borderRadius: 8, padding: 14, cursor: 'pointer', display: 'flex', flexDirection: 'column', gap: 6,
                        transition: 'border-color 0.1s, background 0.1s',
                      }}>
                <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8 }}>
                  <div style={{ fontSize: 14, fontWeight: 600 }}>{t.label}</div>
                  <div style={{ fontSize: 13, fontVariantNumeric: 'tabular-nums', color: 'var(--accent)', fontWeight: 600 }}>
                    {(counts[t.key] || 0).toLocaleString()}
                  </div>
                </div>
                <div style={{ fontSize: 12, color: 'var(--text-2)', lineHeight: 1.5 }}>{t.desc}</div>
                <div style={{ fontSize: 10.5, color: 'var(--text-4)', fontFamily: 'ui-monospace, monospace', marginTop: 2 }}>
                  {t.key}{t.dateCol ? ` · by ${t.dateCol}` : ' · reference'}
                </div>
              </button>
            ))}
          </div>
        </div>
      ))}

      {/* Tables with errors or zero rows — collapsed for honesty */}
      {(Object.keys(errors).length > 0) && (
        <div style={{ marginTop: 20, padding: 12, background: 'var(--bg-elev-1)', border: '1px solid var(--border)', borderRadius: 6 }}>
          <div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-3)', textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 6 }}>Probe errors</div>
          <div style={{ fontSize: 11, color: 'var(--text-3)', fontFamily: 'ui-monospace, monospace' }}>
            {Object.entries(errors).map(([k, v]) => <div key={k}>{k}: {v}</div>)}
          </div>
        </div>
      )}
      <style>{`.dr2-card:hover { border-color: var(--accent); background: var(--bg-elev-1); }`}</style>
    </DR2Shell>
  );
}

// ── Distinct-value lookup for filter dropdowns ──────────────────────────
// Pulls up to 1000 distinct values for a column under the current company.
// Cached in state per (table, col) so it runs once per session per filter.
async function fetchDistinct(db, tableKey, col, companyId) {
  const { data, error } = await db.from(tableKey)
    .select(col).eq('company_id', companyId)
    .not(col, 'is', null).limit(5000);
  if (error || !data) return [];
  const set = new Map();
  data.forEach(r => {
    const v = r[col];
    if (v === null || v === undefined || v === '') return;
    set.set(String(v), v);
  });
  return Array.from(set.values()).sort((a, b) =>
    String(a).localeCompare(String(b), undefined, { numeric: true })
  );
}

// ── Sanitize a search term for PostgREST .or(ilike) ─────────────────────
// PostgREST's or() parser uses commas/parens as structural chars. Strip them.
// '*' is the wildcard in PostgREST; escape user '*' so it stays literal.
function sanitizeIlike(q) {
  return String(q || '').replace(/[(),]/g, ' ').replace(/\*/g, '\\*').trim();
}

// ── Cell formatters ─────────────────────────────────────────────────────
function fmtCellDisplay(val, col) {
  if (val === null || val === undefined || val === '')
    return <span style={{ color: 'var(--text-4)' }}>—</span>;
  switch (col.type) {
    case 'date':  return <span style={{ fontFamily: 'ui-monospace, monospace', fontSize: 11 }}>{String(val).slice(0, 10)}</span>;
    case 'mono':  return <span style={{ fontFamily: 'ui-monospace, monospace', fontSize: 11 }}>{val}</span>;
    case 'chip':  return <span className="pc-chip">{val}</span>;
    case 'bool':  return val ? 'Yes' : 'No';
    case 'num': {
      const n = parseFloat(val);
      if (!isFinite(n)) return val;
      const cls = col.signed ? (n >= 0 ? '' : 'var(--danger)') : '';
      return <span style={{ fontFamily: 'ui-monospace, monospace', fontSize: 11.5, color: cls || undefined }}>{fmtUSD(n, { compact: true })}</span>;
    }
    case 'cents': {
      const n = (parseFloat(val) || 0) / 100;
      const cls = col.signed ? (n >= 0 ? '' : 'var(--danger)') : '';
      return <span style={{ fontFamily: 'ui-monospace, monospace', fontSize: 11.5, color: cls || undefined }}>{fmtUSD(n, { compact: true })}</span>;
    }
    case 'pct': {
      const n = parseFloat(val);
      if (!isFinite(n)) return '—';
      return <span style={{ fontFamily: 'ui-monospace, monospace', fontSize: 11.5 }}>{n.toFixed(1)}%</span>;
    }
    default:      return col.ellipsis
      ? <span style={{ display: 'inline-block', maxWidth: col.ellipsis, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', verticalAlign: 'bottom' }}>{val}</span>
      : <span>{val}</span>;
  }
}
function fmtCellCsv(val, col) {
  if (val === null || val === undefined) return '';
  switch (col.type) {
    case 'cents': return ((parseFloat(val) || 0) / 100).toString();
    case 'date':  return String(val).slice(0, 10);
    case 'bool':  return val ? 'true' : 'false';
    default:      return String(val);
  }
}

// ── Build a Supabase query from filter state (shared by grid + export) ──
function buildQuery(db, tableDef, opts) {
  const { companyId, search, filterValues, inFilters, monthSel, customRange, periodResolved } = opts;
  let q = db.from(tableDef.key).select('*', { count: 'exact' }).eq('company_id', companyId);

  // Text search across configured cols
  const term = sanitizeIlike(search);
  if (term && tableDef.searchCols.length) {
    const orStr = tableDef.searchCols.map(c => `${c}.ilike.*${term}*`).join(',');
    q = q.or(orStr);
  }

  // Column filters (exact-match)
  Object.entries(filterValues || {}).forEach(([col, val]) => {
    if (val === '' || val === null || val === undefined) return;
    if (val === '__null__') q = q.is(col, null);
    else q = q.eq(col, val);
  });

  // Multi-value (IN) filters — used by statement drill-throughs to scope GL to
  // the exact account codes that fed a statement line.
  Object.entries(inFilters || {}).forEach(([col, vals]) => {
    if (Array.isArray(vals) && vals.length) q = q.in(col, vals);
  });

  // Date filters (only for tables with a dateCol)
  if (tableDef.dateCol) {
    if (customRange?.start || customRange?.end) {
      if (customRange.start) q = q.gte(tableDef.dateCol, customRange.start);
      if (customRange.end)   q = q.lte(tableDef.dateCol, customRange.end);
    } else if (monthSel && monthSel !== '__all__') {
      // monthSel format: "YYYY-MM"
      const [y, m] = monthSel.split('-').map(Number);
      const start = `${y}-${String(m).padStart(2,'0')}-01`;
      const endDay = new Date(y, m, 0).getDate();
      const end   = `${y}-${String(m).padStart(2,'0')}-${endDay}`;
      q = q.gte(tableDef.dateCol, start).lte(tableDef.dateCol, end);
    } else if (periodResolved?.startDate && periodResolved?.endDate) {
      q = q.gte(tableDef.dateCol, periodResolved.startDate)
           .lte(tableDef.dateCol, periodResolved.endDate);
    }
  }
  return q;
}

// ── CSV helper (chunked) ────────────────────────────────────────────────
const CSV_MAX_ROWS = 100000;
const CSV_CHUNK    = 1000;
function csvEscape(s) {
  if (s === null || s === undefined) return '';
  const t = String(s).replace(/"/g, '""');
  return /[",\n\r]/.test(t) ? `"${t}"` : t;
}
async function exportCsv(db, tableDef, opts, sort) {
  const headerCols = tableDef.columns;
  const lines = [headerCols.map(c => csvEscape(c.label)).join(',')];

  let offset = 0;
  let total = null;
  // First page also gives us count
  while (offset < CSV_MAX_ROWS) {
    const q = buildQuery(db, tableDef, opts)
      .order(sort.col, { ascending: sort.dir === 'asc', nullsFirst: false })
      .range(offset, offset + CSV_CHUNK - 1);
    const { data, error, count } = await q;
    if (error) throw error;
    if (total === null) total = count || 0;
    if (!data || data.length === 0) break;
    for (const row of data) {
      lines.push(headerCols.map(c => csvEscape(fmtCellCsv(row[c.key], c))).join(','));
    }
    offset += data.length;
    if (offset >= total) break;
    if (data.length < CSV_CHUNK) break;
  }
  const truncated = total > CSV_MAX_ROWS;
  return { csv: lines.join('\n'), rows: Math.min(total || 0, CSV_MAX_ROWS), total: total || 0, truncated };
}

// ── The generic table viewer ────────────────────────────────────────────
const PAGE_SIZE_OPTIONS = [25, 50, 100, 250];

function DR2TablePage({ tableKey, companyId, periodResolved, onBack, initialDrill, backToSource }) {
  const def = DR_TABLE_BY_KEY[tableKey];
  if (!def) return <DR2Shell title="Unknown table" subtitle={tableKey} onBack={onBack} />;
  const db  = window.supabaseClient;

  // Seed the date range / account-code filter from a statement drill-through.
  const drillRange = (initialDrill && (initialDrill.start || initialDrill.end))
    ? { start: initialDrill.start || '', end: initialDrill.end || '' } : { start: '', end: '' };
  // codes -> an IN filter. Defaults to account_code (statement drill-throughs);
  // an optional `col` lets a drill target any column (e.g. customer_name on
  // ar_invoices, vendor_name on ap_bills) — same dr= mechanism, not a new one.
  const drillIn = (initialDrill && Array.isArray(initialDrill.codes) && initialDrill.codes.length)
    ? { [initialDrill.col || 'account_code']: initialDrill.codes } : {};

  const [search, setSearch]             = useDR2State('');
  const [searchDebounced, setSearchDeb] = useDR2State('');
  const [filterValues, setFilterValues] = useDR2State({});  // { col: value }
  const [inFilter, setInFilter]         = useDR2State(drillIn); // { col: [values] }
  const [filterChoices, setFilterChoices] = useDR2State({}); // { col: [values] }
  const [monthSel, setMonthSel]         = useDR2State('__all__');
  const [customRange, setCustomRange]   = useDR2State(drillRange);
  const [sort, setSort]                 = useDR2State(def.defaultSort);
  const [page, setPage]                 = useDR2State(0);
  const [pageSize, setPageSize]         = useDR2State(50);

  const [rows, setRows]                 = useDR2State([]);
  const [total, setTotal]               = useDR2State(0);
  const [loading, setLoading]           = useDR2State(true);
  const [err, setErr]                   = useDR2State(null);
  const [exporting, setExporting]       = useDR2State(false);

  // Debounce search
  useDR2Effect(() => {
    const id = setTimeout(() => setSearchDeb(search), 250);
    return () => clearTimeout(id);
  }, [search]);

  // Reset to first page on any filter change
  useDR2Effect(() => { setPage(0); }, [searchDebounced, filterValues, JSON.stringify(inFilter), monthSel, customRange.start, customRange.end, pageSize, tableKey]);

  // Reset internal state when switching tables — but seed the date range /
  // account-code filter from the drill-through so a deep link lands filtered.
  useDR2Effect(() => {
    setSearch(''); setSearchDeb(''); setFilterValues({}); setFilterChoices({});
    setMonthSel('__all__');
    setCustomRange(drillRange);
    setInFilter(drillIn);
    setSort(def.defaultSort); setPage(0); setRows([]); setTotal(0); setErr(null);
  }, [tableKey]);

  // Lazily fetch distinct values for any filter that the user hasn't seen yet.
  useDR2Effect(() => {
    if (!db || !companyId) return;
    let cancelled = false;
    (async () => {
      const missing = (def.filters || []).filter(f => f.source === 'distinct' && !filterChoices[f.col]);
      for (const f of missing) {
        const vals = await fetchDistinct(db, def.key, f.col, companyId);
        if (cancelled) return;
        setFilterChoices(prev => ({ ...prev, [f.col]: vals }));
      }
    })();
    return () => { cancelled = true; };
  }, [db, companyId, tableKey]);

  // The data fetch
  useDR2Effect(() => {
    if (!db || !companyId) { setLoading(false); return; }
    let cancelled = false;
    setLoading(true); setErr(null);
    (async () => {
      try {
        const q = buildQuery(db, def, { companyId, search: searchDebounced, filterValues, inFilters: inFilter, monthSel, customRange, periodResolved })
          .order(sort.col, { ascending: sort.dir === 'asc', nullsFirst: false })
          .range(page * pageSize, page * pageSize + pageSize - 1);
        const { data, error, count } = await q;
        if (cancelled) return;
        if (error) { setErr(error.message); setRows([]); setTotal(0); }
        else { setRows(data || []); setTotal(count || 0); }
      } catch (e) {
        if (!cancelled) { setErr(String(e)); setRows([]); setTotal(0); }
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, [db, companyId, tableKey, searchDebounced, JSON.stringify(filterValues), JSON.stringify(inFilter), monthSel, customRange.start, customRange.end, sort.col, sort.dir, page, pageSize, periodResolved?.startDate, periodResolved?.endDate]);

  // Month options come from the global period.
  const monthOptions = useDR2Memo(() => {
    if (!def.dateCol) return [];
    const months = periodResolved?.months || [];
    return months.map(m => ({
      value: `${m.year}-${String(m.month1).padStart(2,'0')}`,
      label: m.label,
    }));
  }, [periodResolved, def.dateCol]);

  const handleSort = (colKey) => {
    setSort(prev => prev.col === colKey
      ? { col: colKey, dir: prev.dir === 'asc' ? 'desc' : 'asc' }
      : { col: colKey, dir: 'asc' });
  };

  const setFilter = (col, val) => setFilterValues(prev => {
    const next = { ...prev };
    if (val === '' || val === '__any__') delete next[col];
    else next[col] = val;
    return next;
  });

  const clearFilters = () => {
    setSearch(''); setSearchDeb(''); setFilterValues({});
    setMonthSel('__all__'); setCustomRange({ start: '', end: '' });
    setInFilter({});
  };
  const drillCodes = Array.isArray(inFilter.account_code) ? inFilter.account_code : [];

  const doExport = async () => {
    setExporting(true);
    try {
      const { csv, rows: r, total: t, truncated } = await exportCsv(
        db, def,
        { companyId, search: searchDebounced, filterValues, monthSel, customRange, periodResolved },
        sort
      );
      const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
      const url  = URL.createObjectURL(blob);
      const a    = document.createElement('a');
      const stamp = new Date().toISOString().slice(0, 10);
      a.href = url; a.download = `${def.key}_${stamp}.csv`;
      document.body.appendChild(a); a.click(); document.body.removeChild(a);
      setTimeout(() => URL.revokeObjectURL(url), 200);
      if (truncated) {
        alert(`Exported ${r.toLocaleString()} of ${t.toLocaleString()} rows. Export is capped at ${CSV_MAX_ROWS.toLocaleString()} — narrow the filter to get a smaller slice.`);
      }
    } catch (e) {
      alert('Export failed: ' + (e?.message || e));
    } finally {
      setExporting(false);
    }
  };

  const totalPages = Math.max(1, Math.ceil((total || 0) / pageSize));
  const activeFilters = Object.keys(filterValues).length
    + (searchDebounced ? 1 : 0)
    + (monthSel !== '__all__' ? 1 : 0)
    + ((customRange.start || customRange.end) ? 1 : 0)
    + (drillCodes.length ? 1 : 0);

  const periodNote = def.dateCol && monthSel === '__all__' && !customRange.start && !customRange.end && periodResolved
    ? `Showing ${periodResolved.startDate} → ${periodResolved.endDate} (global period)` : null;

  return (
    <DR2Shell
      title={def.label}
      subtitle={def.desc}
      breadcrumb={def.label}
      onBack={onBack}
      action={
        <>
          {periodNote && (
            <div style={{ fontSize: 11, color: 'var(--text-3)', padding: '4px 8px', border: '1px solid var(--border)', borderRadius: 6 }}>{periodNote}</div>
          )}
          <button className="pc-btn-mini ghost" disabled={exporting || total === 0} onClick={doExport} title="Export current view to CSV">
            <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 3v14 M5 12l7 7 7-7 M3 21h18"/></svg>
            {exporting ? 'Exporting…' : 'CSV'}
          </button>
        </>
      }
    >
      {/* Statement drill-through banner */}
      {(drillCodes.length > 0 || (initialDrill && initialDrill.label)) && (
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 12px', marginBottom: 8, background: 'rgba(96,165,250,0.08)', border: '1px solid rgba(96,165,250,0.25)', borderRadius: 8, fontSize: 12.5, color: 'var(--text-2)' }}>
          {backToSource && (
            <button onClick={backToSource.go}
              style={{ display: 'inline-flex', alignItems: 'center', gap: 6, background: '#0d2040', color: 'white', border: 'none', borderRadius: 7, padding: '6px 14px', fontSize: 12, fontWeight: 700, cursor: 'pointer', flexShrink: 0 }}>
              ← Back to {backToSource.label}
            </button>
          )}
          <span>
            Drilled in from <b style={{ color: 'var(--text)' }}>{(initialDrill && initialDrill.label) || 'a statement line'}</b>
            {drillCodes.length > 0 && <> · {drillCodes.length} account{drillCodes.length === 1 ? '' : 's'}</>}
            {(customRange.start || customRange.end) && <> · {customRange.start || '…'} → {customRange.end || '…'}</>}
          </span>
          <button className="pc-btn-mini ghost" style={{ marginLeft: 'auto' }} onClick={clearFilters}>Clear filter</button>
        </div>
      )}

      {/* Filter bar */}
      <Card padding={0}>
        <div style={{ display: 'flex', gap: 8, alignItems: 'center', padding: 10, flexWrap: 'wrap', borderBottom: '1px solid var(--border)' }}>
          <input
            className="pc-period-select"
            placeholder="Search…"
            value={search}
            onChange={e => setSearch(e.target.value)}
            style={{ minWidth: 220, flex: '0 0 auto' }}
          />
          {def.dateCol && (
            <>
              <select className="pc-period-select" value={monthSel}
                      onChange={e => { setMonthSel(e.target.value); setCustomRange({ start: '', end: '' }); }}
                      style={{ minWidth: 160 }}>
                <option value="__all__">All months in period</option>
                {monthOptions.map(m => <option key={m.value} value={m.value}>{m.label}</option>)}
              </select>
              <input type="date" className="pc-period-select" value={customRange.start}
                     onChange={e => { setCustomRange(r => ({ ...r, start: e.target.value })); setMonthSel('__all__'); }}
                     style={{ minWidth: 130 }} title="From date" />
              <input type="date" className="pc-period-select" value={customRange.end}
                     onChange={e => { setCustomRange(r => ({ ...r, end: e.target.value })); setMonthSel('__all__'); }}
                     style={{ minWidth: 130 }} title="To date" />
            </>
          )}
          {(def.filters || []).map(f => (
            <select key={f.col} className="pc-period-select"
                    value={filterValues[f.col] || ''}
                    onChange={e => setFilter(f.col, e.target.value)}
                    style={{ minWidth: 140 }}>
              <option value="">All {f.label.toLowerCase()}</option>
              {(filterChoices[f.col] || []).map(v => (
                <option key={String(v)} value={String(v)}>{String(v)}</option>
              ))}
            </select>
          ))}
          {activeFilters > 0 && (
            <button className="pc-btn-mini ghost" onClick={clearFilters} style={{ marginLeft: 'auto' }}>
              Clear ({activeFilters})
            </button>
          )}
        </div>

        {/* Result summary */}
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '8px 12px', fontSize: 11.5, color: 'var(--text-3)', borderBottom: '1px solid var(--border)' }}>
          <div>
            {loading
              ? 'Loading…'
              : err
                ? <span style={{ color: 'var(--danger)' }}>Error: {err}</span>
                : <>
                    {total.toLocaleString()} row{total === 1 ? '' : 's'}
                    {total > 0 && <> · page {page + 1} of {totalPages.toLocaleString()}</>}
                  </>}
          </div>
          <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
            <label style={{ fontSize: 11, color: 'var(--text-3)' }}>Page size</label>
            <select className="pc-period-select" value={pageSize} onChange={e => setPageSize(Number(e.target.value))} style={{ padding: '2px 6px' }}>
              {PAGE_SIZE_OPTIONS.map(s => <option key={s} value={s}>{s}</option>)}
            </select>
            <button className="pc-btn-mini ghost" disabled={page === 0 || loading} onClick={() => setPage(p => Math.max(0, p - 1))}>‹ Prev</button>
            <button className="pc-btn-mini ghost" disabled={page + 1 >= totalPages || loading} onClick={() => setPage(p => p + 1)}>Next ›</button>
          </div>
        </div>

        {/* The grid */}
        <div style={{ overflowX: 'auto', maxHeight: 'calc(100vh - 320px)', overflowY: 'auto' }}>
          <table className="pc-table compact">
            <thead style={{ position: 'sticky', top: 0, background: 'var(--bg-card-solid)', zIndex: 1 }}>
              <tr>
                {def.columns.map(c => (
                  <th key={c.key} style={{
                    textAlign: c.align || 'left', cursor: 'pointer', userSelect: 'none',
                    width: c.w || undefined, whiteSpace: 'nowrap',
                  }} onClick={() => handleSort(c.key)} title={`Sort by ${c.label}`}>
                    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
                      {c.label}
                      <span style={{ fontSize: 9, color: sort.col === c.key ? 'var(--accent)' : 'var(--text-4)' }}>
                        {sort.col === c.key ? (sort.dir === 'asc' ? '▲' : '▼') : '↕'}
                      </span>
                    </span>
                  </th>
                ))}
              </tr>
            </thead>
            <tbody>
              {rows.length === 0 && !loading && (
                <tr><td colSpan={def.columns.length} style={{ padding: 32, textAlign: 'center', color: 'var(--text-3)' }}>
                  {activeFilters ? 'No rows match these filters.' : 'No rows.'}
                </td></tr>
              )}
              {rows.map((r, i) => (
                <tr key={r.id || (page * pageSize + i)}>
                  {def.columns.map(c => (
                    <td key={c.key} style={{ textAlign: c.align || 'left' }}>
                      {fmtCellDisplay(r[c.key], c)}
                    </td>
                  ))}
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </Card>

      {/* Footer pagination repeated for convenience on long pages */}
      {total > pageSize && (
        <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, alignItems: 'center', marginTop: 10, fontSize: 11.5, color: 'var(--text-3)' }}>
          <span>Page {page + 1} of {totalPages.toLocaleString()}</span>
          <button className="pc-btn-mini ghost" disabled={page === 0 || loading} onClick={() => setPage(0)}>« First</button>
          <button className="pc-btn-mini ghost" disabled={page === 0 || loading} onClick={() => setPage(p => Math.max(0, p - 1))}>‹ Prev</button>
          <button className="pc-btn-mini ghost" disabled={page + 1 >= totalPages || loading} onClick={() => setPage(p => p + 1)}>Next ›</button>
          <button className="pc-btn-mini ghost" disabled={page + 1 >= totalPages || loading} onClick={() => setPage(totalPages - 1)}>Last »</button>
        </div>
      )}
    </DR2Shell>
  );
}

// ── Top-level Data Room page (index ↔ table viewer) ─────────────────────
function DataRoomPage({ scopedCompanyId, periodResolved, companyProfile, setPage, drill, tableSlug }) {
  const companyId = dr2UseCompanyId(scopedCompanyId);
  // A drill-through opens a specific table. Two URL conventions are honored:
  //   • `drill` (?dr= query) = { t: tableKey, codes?, start?, end?, label? } —
  //     statement line drills, carry an account-code + period filter.
  //   • `tableSlug` (path segment, e.g. /data-room/gl-transactions) — selects a
  //     table with no filter (synthesis "View details", direct links). Dash and
  //     underscore are normalized so the router slug matches the table key.
  const slugTable = tableSlug ? (DR_TABLE_BY_KEY[tableSlug] ? tableSlug
    : (DR_TABLE_BY_KEY[String(tableSlug).replace(/-/g, '_')] ? String(tableSlug).replace(/-/g, '_') : null)) : null;
  const drillTable = (drill && DR_TABLE_BY_KEY[drill.t]) ? drill.t : slugTable;
  const [active, setActive] = useDR2State(drillTable);  // tableKey or null = index
  const [drillFilter, setDrillFilter] = useDR2State((drill && DR_TABLE_BY_KEY[drill.t]) ? drill : null);
  const drillSig = (drill ? JSON.stringify(drill) : '') + '|' + (slugTable || '');

  // A new deep-link (?dr= filter or path slug) opens its table.
  useDR2Effect(() => {
    if (drillTable) {
      setActive(drillTable);
      setDrillFilter((drill && DR_TABLE_BY_KEY[drill.t]) ? drill : null);
    }
  }, [drillSig]);

  // Reset selection when the scoped company changes — unless a drill targets it.
  useDR2Effect(() => { if (!drillTable) { setActive(null); setDrillFilter(null); } }, [companyId]);

  if (!companyId) {
    return (
      <DR2Shell title="Data Room" subtitle="Resolving company…">
        <Card><div style={{ padding: 32, textAlign: 'center', color: 'var(--text-3)' }}>Waiting for company context…</div></Card>
      </DR2Shell>
    );
  }

  if (!active) {
    return <DR2IndexPage companyId={companyId} companyProfile={companyProfile} onOpen={(k) => { setActive(k); setDrillFilter(null); }} onLeave={() => window.history.back()} />;
  }

  return (
    <DR2TablePage
      key={active}
      tableKey={active}
      companyId={companyId}
      periodResolved={periodResolved}
      initialDrill={(drillFilter && drillFilter.t === active) ? drillFilter : null}
      backToSource={(drillFilter && drillFilter.t === active && drillFilter.back && setPage)
        ? { label: drillFilter.backLabel || "statement", go: () => setPage(drillFilter.back) }
        : null}
      onBack={() => { setActive(null); setDrillFilter(null); }}
    />
  );
}

Object.assign(window, {
  DataRoomPage,
  PerduraDataRoom: { tables: DR_TABLES, tableByKey: DR_TABLE_BY_KEY },
});
