const { useState, useEffect, useLayoutEffect, useCallback, useMemo, useRef } = React;

const API_BASE = '';

function parseApiErrorBody(text, status) {
  if (!text) {
    if (status === 401) {
      return {
        code: 'unauthorized',
        message: 'Sessão expirada ou sem permissão. Inicie sessão novamente.',
        reconsentUrl: '/login',
      };
    }
    if (status === 403) {
      return { code: 'forbidden', message: 'Não tem permissão para aceder a estes dados no SharePoint.' };
    }
    if (status === 404 || status === 405) {
      return {
        code: 'endpoint_missing',
        message: 'Endpoint em falta. Reinicie a API MyHabita e tente novamente.',
      };
    }
    return { code: 'unknown', message: 'Erro ao comunicar com o servidor.' };
  }

  if (
    text.includes('IDW10502') ||
    text.includes('MsalUiRequiredException') ||
    text.includes('MicrosoftIdentityWebChallengeUserException')
  ) {
    return {
      code: 'consent_required',
      message:
        'É necessário autorizar o acesso ao SharePoint. Saia da sessão, inicie sessão novamente e aceite a permissão Sites.ReadWrite.All. ' +
        'Se o ecrã de consentimento não aparecer, use «Autorizar SharePoint».',
      reconsentUrl: '/reconsent',
    };
  }

  if (text.includes('IDW10503')) {
    return {
      code: 'auth_config',
      message: 'Erro de configuração de autenticação. Reinicie a aplicação e contacte o administrador.',
    };
  }

  try {
    const json = JSON.parse(text);
    if (json.error === 'consent_required' || json.code === 'consent_required') {
      return {
        code: 'consent_required',
        message: json.message || 'Autorização SharePoint necessária.',
        reconsentUrl: json.reconsentUrl || '/reconsent',
      };
    }
    const message = json.message || json.error || json.title || json.detail;
    if (typeof message === 'string' && message.length > 0) {
      return { code: json.code || json.error || 'api_error', message };
    }
  } catch { /* not JSON */ }

  // SPA fallback / stale server often returns HTML instead of JSON.
  if (/^\s*</.test(text) || text.includes('<!DOCTYPE') || text.includes('<html')) {
    if (status === 404 || status === 405) {
      return {
        code: 'endpoint_missing',
        message: 'Endpoint em falta. Reinicie a API MyHabita e tente novamente.',
      };
    }
    return {
      code: 'server',
      message: 'Resposta inválida do servidor. Reinicie a API e atualize a página.',
    };
  }

  if (text.includes('ODataError')) {
    const short = text.match(/ODataError:\s*([^\r\n]+)/);
    return { code: 'sharepoint', message: short ? short[1] : 'Erro ao consultar SharePoint.' };
  }

  const firstLine = text.split('\n')[0].trim();
  if (firstLine.includes('Exception:')) {
    const after = firstLine.split('Exception:').pop()?.trim();
    if (after) return { code: 'server', message: after };
  }

  return { code: 'server', message: firstLine.length > 280 ? firstLine.slice(0, 280) + '…' : firstLine };
}

async function apiFetch(path, options = {}) {
  const headers = { 'Content-Type': 'application/json', ...options.headers };
  const viewAsEmail = window.__myHabitaViewAsEmail;
  if (viewAsEmail) headers['X-View-As-Email'] = viewAsEmail;

  const res = await fetch(`${API_BASE}${path}`, {
    credentials: 'include',
    ...options,
    headers,
  });
  if (!res.ok) {
    const text = await res.text();
    const parsed = parseApiErrorBody(text, res.status);
    const err = new Error(parsed.message);
    err.code = parsed.code;
    err.reconsentUrl = parsed.reconsentUrl;
    err.status = res.status;

    // Mid-session Graph/cookie death → one clean re-login (avoid half-dead SPA).
    if (
      !options.skipAuthRedirect &&
      (res.status === 401 || parsed.code === 'consent_required' || parsed.code === 'unauthorized') &&
      !window.__myHabitaAuthRedirecting
    ) {
      window.__myHabitaAuthRedirecting = true;
      const target = parsed.reconsentUrl || (parsed.code === 'consent_required' ? '/reconsent' : '/login');
      window.location.href = target;
    }

    throw err;
  }
  if (res.status === 204) return null;
  return res.json();
}

/** Keep auth cookie sliding + detect dead Graph tokens while the tab is visible. */
function startAuthKeepAlive() {
  if (window.__myHabitaKeepAliveStarted) return;
  window.__myHabitaKeepAliveStarted = true;
  const PING_MS = 5 * 60 * 1000;

  const ping = () => {
    if (document.visibilityState === 'hidden') return;
    fetch('/api/auth/me', {
      credentials: 'include',
      headers: { Accept: 'application/json' },
    }).then(res => {
      if (res.status === 401 && !window.__myHabitaAuthRedirecting) {
        window.__myHabitaAuthRedirecting = true;
        window.location.href = '/login';
      }
    }).catch(() => { /* ignore transient */ });
  };

  setInterval(ping, PING_MS);
  document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'visible') ping();
  });
}

function ErrorBanner({ error, onDismiss }) {
  if (!error) return null;
  const message = typeof error === 'string' ? error : error.message;
  const code = typeof error === 'object' ? error.code : null;
  const reconsentUrl = typeof error === 'object' ? error.reconsentUrl : null;

  return (
    <div className="error-msg">
      <p>{message}</p>
      {(code === 'consent_required' || code === 'unauthorized') && (
        <div className="error-actions">
          {reconsentUrl && (
            <a className="btn btn-primary" href={reconsentUrl}>Autorizar SharePoint</a>
          )}
          <a className="btn btn-secondary" href="/logout">Sair e voltar a entrar</a>
        </div>
      )}
      {onDismiss && (
        <button type="button" className="btn btn-secondary btn-sm" onClick={onDismiss} style={{ marginTop: '0.5rem' }}>
          Fechar
        </button>
      )}
    </div>
  );
}

const TOAST_EVENT = 'myhabita:toast';
const TOAST_DEFAULT_DURATION = {
  success: 4000,
  info: 4000,
  warning: 5500,
  error: 7000,
};
const TOAST_MAX_VISIBLE = 5;
let toastSeq = 0;

function emitToast(payload) {
  if (typeof window === 'undefined') return null;
  const detail = { ...payload, id: payload.id || `toast-${++toastSeq}` };
  window.dispatchEvent(new CustomEvent(TOAST_EVENT, { detail }));
  return detail.id;
}

const toast = {
  show(opts = {}) {
    const type = ['success', 'error', 'warning', 'info'].includes(opts.type) ? opts.type : 'info';
    const message = String(opts.message || opts.text || '').trim();
    if (!message) return null;
    const permanent = Boolean(opts.permanent) || opts.duration === 0;
    const duration = permanent
      ? 0
      : (typeof opts.duration === 'number' ? opts.duration : TOAST_DEFAULT_DURATION[type]);
    return emitToast({
      type,
      message,
      title: opts.title || null,
      permanent,
      duration,
    });
  },
  success(message, opts = {}) { return toast.show({ ...opts, type: 'success', message }); },
  error(message, opts = {}) { return toast.show({ ...opts, type: 'error', message }); },
  warning(message, opts = {}) { return toast.show({ ...opts, type: 'warning', message }); },
  info(message, opts = {}) { return toast.show({ ...opts, type: 'info', message }); },
};

window.toast = toast;

const APP_RELEASE = {
  id: '2026.10.02-page',
  title: 'Nova versão MYHABITA',
  dateLabel: '2 de outubro de 2026',
  intro: 'Correções no Dashboard, SOI, Mediação, Recrutamentos e Calendário — o mix já não corta valores, o SOI muda de linha quando é estreito, a Mediação mostra o total de páginas, e o administrador pode apagar um evento sem avisar ninguém.',
  sections: [
    {
      system: 'Dashboard',
      items: [
        'No Mix Vendas / Arrendamento, os valores de Venda e Arrend. ficam inteiros em ecrãs grandes — deixam de ser cortados.',
      ],
    },
    {
      system: 'SOI',
      items: [
        'Nas células estreitas, Faturação e os outros indicadores passam para duas linhas; quando há espaço, voltam a uma só linha.',
      ],
    },
    {
      system: 'Mediação',
      items: [
        'O rodapé da lista de negócios volta a mostrar o total: Linhas 24 / 31 · Página 1 / 2.',
      ],
    },
    {
      system: 'Recrutamentos',
      items: [
        'A lista de candidatos volta a funcionar em produção (listas Recrutamentos e Agenda ligadas).',
        'Se a agenda falhar, a tabela de candidatos continua a aparecer.',
      ],
    },
    {
      system: 'Calendário',
      items: [
        'O ficheiro do calendário fica guardado no servidor e já não desaparece no publish.',
        'Administrador: ícone do caixote no painel do evento. Apaga em qualquer estado, sem notificar ninguém.',
        'Cancelar visita continua a pedir motivo e a avisar o consultor.',
      ],
    },
  ],
};

const RELEASE_SEEN_KEY = 'myhabita.release.seen';

function hasSeenRelease(id) {
  try {
    return window.localStorage.getItem(RELEASE_SEEN_KEY) === id;
  } catch {
    return true;
  }
}

function markReleaseSeen(id) {
  try {
    window.localStorage.setItem(RELEASE_SEEN_KEY, id);
  } catch { /* ignore */ }
  window.dispatchEvent(new CustomEvent('myhabita:release-seen', { detail: { id } }));
}

function getReleaseNotification() {
  return {
    id: `release-${APP_RELEASE.id}`,
    tipo: 'release',
    titulo: APP_RELEASE.title,
    mensagem: APP_RELEASE.intro,
    actionHint: APP_RELEASE.dateLabel,
    severity: 'info',
    requiresAction: !hasSeenRelease(APP_RELEASE.id),
  };
}

function withLatestReleaseNotification(items) {
  const release = getReleaseNotification();
  const rest = (items || []).filter(n => {
    const tipo = String(n.tipo || n.Tipo || '');
    const id = String(n.id || n.Id || '');
    return !/^release$/i.test(tipo) && !id.startsWith('release-');
  });
  return [release, ...rest];
}

function ReleaseNotesModal({ release, onClose }) {
  const dismiss = useOverlayDismiss(onClose);
  if (!release) return null;
  return ReactDOM.createPortal(
    <div
      className="confirm-dialog-overlay release-notes-overlay"
      role="presentation"
      {...dismiss}
    >
      <div
        className="confirm-dialog release-notes-dialog"
        role="dialog"
        aria-modal="true"
        aria-labelledby="release-notes-title"
        onClick={e => e.stopPropagation()}
      >
        <h3 id="release-notes-title">{release.title}</h3>
        <p className="release-notes-date">{release.dateLabel}</p>
        <p>{release.intro}</p>
        <div className="release-notes-body">
          {release.sections.map(section => (
            <section key={section.system} className="release-notes-section">
              <h4>{section.system}</h4>
              <ul>
                {section.items.map(item => (
                  <li key={item}>{item}</li>
                ))}
              </ul>
            </section>
          ))}
        </div>
        <div className="confirm-dialog-actions">
          <button type="button" className="btn btn-primary" onClick={onClose}>
            Entendi
          </button>
        </div>
      </div>
    </div>,
    document.body
  );
}

function toastTypeMeta(type) {
  switch (type) {
    case 'success':
      return { icon: 'checkmark', label: 'Sucesso' };
    case 'error':
      return { icon: 'circleXmark', label: 'Erro' };
    case 'warning':
      return { icon: 'triangleExclamation', label: 'Aviso' };
    default:
      return { icon: 'circleInfo', label: 'Informação' };
  }
}

function useToastAnchor() {
  useEffect(() => {
    const place = () => {
      const bar = document.querySelector('main.main > header.topbar, main.main > .topbar');
      const top = bar ? Math.ceil(bar.getBoundingClientRect().bottom + 12) : 16;
      document.documentElement.style.setProperty('--toast-top', `${Math.max(16, top)}px`);
    };
    place();
    const root = document.getElementById('root');
    const mo = new MutationObserver(place);
    if (root) mo.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
    window.addEventListener('resize', place);
    return () => {
      mo.disconnect();
      window.removeEventListener('resize', place);
    };
  }, []);
}

function ToastHost() {
  const [items, setItems] = useState([]);
  const timersRef = useRef(new Map());
  useToastAnchor();

  const dismiss = useCallback((id) => {
    const t = timersRef.current.get(id);
    if (t) {
      clearTimeout(t);
      timersRef.current.delete(id);
    }
    setItems(prev => prev.map(it => (it.id === id ? { ...it, leaving: true } : it)));
    window.setTimeout(() => {
      setItems(prev => prev.filter(it => it.id !== id));
    }, 220);
  }, []);

  useEffect(() => {
    const onToast = (e) => {
      const detail = e.detail || {};
      if (!detail.id || !detail.message) return;
      setItems(prev => {
        const next = [{ ...detail, leaving: false }, ...prev];
        // Drop oldest timed toasts when over the cap; keep permanent ones when possible.
        if (next.length <= TOAST_MAX_VISIBLE) return next;
        const kept = [];
        const overflow = [];
        for (const it of next) {
          if (kept.length < TOAST_MAX_VISIBLE) kept.push(it);
          else overflow.push(it);
        }
        overflow
          .filter(it => !it.permanent)
          .forEach(it => {
            const tm = timersRef.current.get(it.id);
            if (tm) {
              clearTimeout(tm);
              timersRef.current.delete(it.id);
            }
          });
        return kept;
      });
      if (!detail.permanent && detail.duration > 0) {
        const existing = timersRef.current.get(detail.id);
        if (existing) clearTimeout(existing);
        const tm = window.setTimeout(() => dismiss(detail.id), detail.duration);
        timersRef.current.set(detail.id, tm);
      }
    };
    window.addEventListener(TOAST_EVENT, onToast);
    return () => {
      window.removeEventListener(TOAST_EVENT, onToast);
      timersRef.current.forEach(tm => clearTimeout(tm));
      timersRef.current.clear();
    };
  }, [dismiss]);

  if (!items.length) return null;

  return ReactDOM.createPortal(
    <div className="toast-host" aria-live="polite" aria-relevant="additions text">
      {items.map(item => {
        const meta = toastTypeMeta(item.type);
        return (
          <div
            key={item.id}
            className={`toast toast-${item.type}${item.leaving ? ' is-leaving' : ''}${item.permanent ? ' is-permanent' : ''}`}
            role={item.type === 'error' || item.type === 'warning' ? 'alert' : 'status'}
          >
            <span className="toast-icon" aria-hidden="true">
              <Icon name={meta.icon} size={16} />
            </span>
            <div className="toast-body">
              {item.title ? <div className="toast-title">{item.title}</div> : (
                <div className="toast-kicker">{meta.label}</div>
              )}
              <div className="toast-message">{item.message}</div>
            </div>
            <button
              type="button"
              className="toast-close"
              aria-label="Fechar notificação"
              onClick={() => dismiss(item.id)}
            >
              <Icon name="close" size={12} />
            </button>
          </div>
        );
      })}
    </div>,
    document.body
  );
}

function formatCurrency(value) {
  if (value == null) return '—';
  return new Intl.NumberFormat('pt-PT', { style: 'currency', currency: 'EUR' }).format(value);
}

function formatNumber(value) {
  if (value == null || value === '') return '—';
  const n = typeof value === 'number' ? value : parseFloat(String(value).replace(/\s/g, '').replace(',', '.'));
  if (Number.isNaN(n)) return String(value);
  return new Intl.NumberFormat('pt-PT', { maximumFractionDigits: 6 }).format(n);
}

function parseNumericValue(value) {
  const n = coerceRangeNumber(value);
  return n == null ? NaN : n;
}
function formatPercent(value) {
  const n = parseNumericValue(value);
  if (Number.isNaN(n)) return '—';
  return `${formatNumber(n)} %`;
}

function formatDate(value) {
  if (!value) return '—';
  const d = value instanceof Date ? value : new Date(value);
  if (Number.isNaN(d.getTime())) return String(value);
  return d.toLocaleDateString('pt-PT', { day: '2-digit', month: '2-digit', year: 'numeric' });
}

const GRID_COLUMN_FORMAT_OPTIONS = [
  { value: '', label: 'Auto' },
  { value: 'text', label: 'Texto' },
  { value: 'number', label: 'Número' },
  { value: 'money', label: 'Moeda (0 000,00 €)' },
  { value: 'percent', label: 'Percentagem' },
  { value: 'date', label: 'Data' },
  { value: 'boolean', label: 'Sim / Não' },
];

function isSpSystemColumn(col) {
  return !!(col?.isSystem ?? col?.IsSystem ?? col?.readOnly ?? col?.ReadOnly);
}

function defaultFormatForSpColumn(col, fieldKey) {
  const kind = String(col?.columnKind || col?.ColumnKind || '').toLowerCase();
  if (kind === 'currency') return 'money';
  if (kind === 'datetime' || kind === 'date') return 'date';
  if (kind === 'number') return 'number';
  if (kind === 'boolean') return 'boolean';
  const k = String(fieldKey || '').toLowerCase();
  if (
    k.includes('comiss') ||
    k.includes('montante') ||
    k.includes('valor') ||
    k.includes('preco') ||
    k.includes('preço')
  ) return 'money';
  if (k.includes('data') || k.endsWith('date') || k.includes('admiss')) return 'date';
  if (k.includes('ano') || k.includes('semana') || k.includes('percent')) return 'number';
  return null;
}

function defaultDisplayPathForSpKind(col) {
  const kind = String(col?.columnKind || col?.ColumnKind || '').toLowerCase();
  if (kind === 'lookup') {
    const lookupCol = col?.lookupColumnName || col?.LookupColumnName;
    if (lookupCol && String(lookupCol).toLowerCase() === 'title') return 'Title';
    return 'lookupValue';
  }
  if (kind === 'person') return 'displayName';
  if (kind === 'location' || kind === 'geolocation') return 'DisplayName';
  return null;
}

/** Pipeline-style defaults: Utilizador visible, Sistema hidden, formats from column kind. */
function buildDefaultGridColumnsFromSp(spColumns) {
  return (spColumns || [])
    .filter(c => {
      const name = c.name || c.Name || '';
      return name && !name.startsWith('_');
    })
    .map((col, i) => {
      const name = col.name || col.Name;
      const isSystem = isSpSystemColumn(col);
      const forceVisible = name.toLowerCase() === 'title';
      return {
        key: name,
        label: col.displayName || col.DisplayName || humanizeSpFieldKey(name),
        visible: forceVisible || !isSystem,
        order: i,
        displayPath: defaultDisplayPathForSpKind(col),
        format: defaultFormatForSpColumn(col, name),
        choices: col.choices || col.Choices || null,
        columnKind: col.columnKind || col.ColumnKind || null,
        allowMultiple: col.allowMultiple === true || col.AllowMultiple === true,
      };
    });
}

/** Prefer fallback labels/order when keys match; keep SP display names for FIELD_* etc. */
function mergeFallbackOntoSpColumns(spDefs, fallbackMap) {
  const seeded = toColumnDefs(spDefs);
  const fallbackDefs = toColumnDefs(fallbackMap);
  if (!fallbackDefs.length) return seeded;

  const byKey = new Map(seeded.map(c => [String(c.key).toLowerCase(), { ...c }]));
  const ordered = [];
  const used = new Set();

  fallbackDefs.forEach((fb, i) => {
    const hit = byKey.get(String(fb.key).toLowerCase());
    if (!hit) return;
    ordered.push({
      ...hit,
      label: fb.label || hit.label,
      visible: true,
      order: i,
      displayPath: fb.displayPath || hit.displayPath,
      format: fb.format || hit.format,
    });
    used.add(String(hit.key).toLowerCase());
  });

  seeded.forEach((col) => {
    const k = String(col.key).toLowerCase();
    if (used.has(k)) return;
    ordered.push({ ...col, order: ordered.length });
  });

  return ordered;
}

/**
 * Resolve grid columns: settings → SharePoint display names → fallback map.
 * `locked` means loadData must not replace labels with inferred FIELD_* keys.
 */
async function fetchListGridColumns(listKey, fallbackMap) {
  try {
    const res = await apiFetch(`/api/settings/lists/by-key/${encodeURIComponent(listKey)}/grid-columns`);
    const cols = res.columns || res.Columns || [];
    if (Array.isArray(cols) && cols.length > 0) {
      return {
        defs: toColumnDefs(cols),
        cardLayout: normalizePhoneCardLayout(res.cardLayout || res.CardLayout),
        fromSettings: true,
        locked: true,
      };
    }
  } catch { /* fall back */ }

  try {
    const spCols = await apiFetch(`/api/settings/lists/by-key/${encodeURIComponent(listKey)}/columns`);
    const list = Array.isArray(spCols) ? spCols : (spCols?.columns || spCols?.Columns || []);
    if (Array.isArray(list) && list.length > 0) {
      const seeded = buildDefaultGridColumnsFromSp(list);
      const merged = mergeFallbackOntoSpColumns(seeded, fallbackMap);
      return { defs: toColumnDefs(merged), cardLayout: emptyPhoneCardLayout(), fromSettings: false, locked: true };
    }
  } catch { /* fall back */ }

  return { defs: toColumnDefs(fallbackMap), cardLayout: emptyPhoneCardLayout(), fromSettings: false, locked: false };
}

function applyUtilizadorVisibilityToGridColumns(configured, spColumns, visible) {
  const byKey = new Map((configured || []).map(c => [c.key || c.Key, c]));
  const names = (spColumns || [])
    .map(c => c.name || c.Name)
    .filter(n => n && !String(n).startsWith('_'));

  names.forEach((name, i) => {
    const sp = (spColumns || []).find(c => (c.name || c.Name) === name);
    const isSystem = isSpSystemColumn(sp || {});
    const existing = byKey.get(name);
    if (isSystem && name.toLowerCase() !== 'title') {
      byKey.set(name, {
        key: name,
        label: existing?.label || existing?.Label || sp?.displayName || sp?.DisplayName || humanizeSpFieldKey(name),
        visible: false,
        order: existing?.order ?? existing?.Order ?? i,
        displayPath: existing?.displayPath ?? existing?.DisplayPath ?? defaultDisplayPathForSpKind(sp || {}),
        format: existing?.format ?? existing?.Format ?? defaultFormatForSpColumn(sp || {}, name),
      });
      return;
    }
    byKey.set(name, {
      key: name,
      label: existing?.label || existing?.Label || sp?.displayName || sp?.DisplayName || humanizeSpFieldKey(name),
      visible,
      order: existing?.order ?? existing?.Order ?? i,
      displayPath: existing?.displayPath ?? existing?.DisplayPath ?? defaultDisplayPathForSpKind(sp || {}),
      format: existing?.format ?? existing?.Format ?? defaultFormatForSpColumn(sp || {}, name),
    });
  });

  return [...byKey.values()]
    .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
    .map((c, i) => ({ ...c, order: i }));
}

function formatCellValue(value, fieldKey, format) {
  if (value == null || value === '') return '—';
  const key = (fieldKey || '').toLowerCase();
  const fmt = String(format || '').toLowerCase().trim();
  // Money/number formats are measures — never blank them as unresolved lookup IDs.
  const numericFmt = fmt === 'money' || fmt === 'currency' || fmt === 'number' || fmt === 'percent';
  if (!numericFmt && isLikelyLookupIdValue(value)) return '—';
  let normalized = value;
  if (typeof value === 'string') normalized = normalizeNbspText(value);

  // Years: keep as plain 4-digit text (avoid pt-PT "2 024" thousand grouping).
  if (key === 'ano' || key.includes('ano_x0020') || /(^|_)ano$/.test(key)) {
    const cleaned = normalizeNbspText(normalized);
    if (cleaned == null || cleaned === '') return '—';
    const digits = String(cleaned).replace(/[^\d]/g, '');
    if (/^\d{4}$/.test(digits)) return digits;
    return String(cleaned);
  }

  if (Array.isArray(normalized)) {
    const parts = normalized
      .map(v => {
        if (v == null || v === '') return null;
        if (typeof v === 'object') {
          return v.DisplayName || v.displayName || v.lookupValue || v.value || v.Title || v.email || null;
        }
        return String(v);
      })
      .filter(Boolean);
    return parts.length ? parts.join(', ') : '—';
  }

  if (typeof normalized === 'string' && normalized.trim().startsWith('[')) {
    try {
      const parsed = JSON.parse(normalized);
      if (Array.isArray(parsed)) return formatCellValue(parsed, fieldKey, format);
    } catch { /* keep raw */ }
  }
  if (typeof normalized === 'string' && normalized.trim().startsWith('{') && normalized.includes('DisplayName')) {
    try {
      const obj = JSON.parse(normalized);
      if (obj.DisplayName) return String(obj.DisplayName);
    } catch { /* keep raw */ }
  }
  if (normalized && typeof normalized === 'object' && normalized.DisplayName) {
    return String(normalized.DisplayName);
  }

  if (fmt === 'text') {
    if (typeof normalized === 'object') {
      return normalized.displayName || normalized.DisplayName || normalized.lookupValue || normalized.value || JSON.stringify(normalized);
    }
    return String(normalized);
  }
  if (fmt === 'money' || fmt === 'currency') {
    const n = parseNumericValue(normalized);
    return Number.isNaN(n) ? String(normalized) : formatCurrency(n);
  }
  if (fmt === 'number') {
    return formatNumber(normalized);
  }
  if (fmt === 'percent') {
    const n = parseNumericValue(normalized);
    if (Number.isNaN(n)) return String(normalized);
    const pct = n <= 1 ? n * 100 : n;
    return `${Math.round(pct * 100) / 100}%`;
  }
  if (fmt === 'date') {
    const d = new Date(normalized);
    return Number.isNaN(d.getTime()) ? String(normalized) : formatDate(normalized);
  }
  if (fmt === 'boolean' || fmt === 'bool') {
    if (normalized === true || normalized === 'true' || normalized === 1 || normalized === '1') return 'Sim';
    if (normalized === false || normalized === 'false' || normalized === 0 || normalized === '0') return 'Não';
    return String(normalized);
  }

  if (key.includes('data') || key.endsWith('date') || key.includes('periodo')) {
    const d = new Date(normalized);
    if (!Number.isNaN(d.getTime())) return formatDate(normalized);
  }
  if (key.includes('perc') || key.includes('partilha') || key.includes('probabil') || key.includes('parcentagem') || key.includes('rendimento')) {
    const n = parseNumericValue(normalized);
    if (!Number.isNaN(n)) {
      const pct = n <= 1 ? n * 100 : n;
      return `${Math.round(pct * 100) / 100}%`;
    }
  }
  if (key.includes('comissao') || key.includes('comiss') || key.includes('montante') || key.includes('valor')) {
    const n = parseNumericValue(normalized);
    if (!Number.isNaN(n)) return formatCurrency(n);
  }
  if (typeof normalized === 'object') {
    if (Object.keys(normalized).length === 0) return '—';
    if (normalized.displayName) return normalized.displayName;
    if (normalized.email) return normalized.email;
    if (normalized.lookupValue) return normalized.lookupValue;
    if (normalized.value != null) return String(normalized.value);
    return JSON.stringify(normalized);
  }
  return String(normalized);
}

function valueTone(value) {
  const v = String(value ?? '').trim().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
  if (!v) return '';
  if (v === 'venda') return 'venda';
  if (v === 'arrendamento') return 'arrendamento';
  if (v === 'em curso' || v === 'ativa' || v === 'disponivel' || v === 'reservado' || v === 'off-market' || v === 'off market') {
    return 'em-curso';
  }
  if (v === 'concluido' || v === 'ganha' || v === 'ganho' || v === 'vendido') return 'concluido';
  if (v === 'perdido' || v === 'perdida' || v === 'retirado') return 'perdido';
  if (v === 'alta') return 'em-curso';
  if (v === 'media') return 'arrendamento';
  if (v === 'baixa') return 'perdido';
  return '';
}

function statusClass(estado) {
  if (!estado) return '';
  const tone = valueTone(estado);
  if (tone) return `status-${tone}`;
  const e = String(estado).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
  if (e.includes('aprov') || e.includes('conclu') || e.includes('ganh') || e === 'sim' || e === 'validado') {
    return 'status-concluido';
  }
  if (e.includes('reprov') || e.includes('perdid') || e.includes('recus') || e === 'nao' || e === 'não') {
    return 'status-perdido';
  }
  if (e.includes('pendente') || e.includes('aguarda') || e.includes('analise') || e.includes('em curso') || e.includes('aberto') || e === 'ativa') {
    return 'status-em-curso';
  }
  return 'status-em-curso';
}

function AppTooltip({ children, label, className = '' }) {
  const [pos, setPos] = useState(null);
  const hostRef = useRef(null);
  const tipRef = useRef(null);

  const place = useCallback((el) => {
    const host = el || hostRef.current;
    if (!host) return;
    const target = host.firstElementChild || host;
    const r = target.getBoundingClientRect();
    const gap = 8;
    const preferBelow = r.top < 44;
    let left = r.left + r.width / 2;
    const tipW = tipRef.current?.offsetWidth;
    if (tipW) {
      const half = tipW / 2;
      left = Math.min(window.innerWidth - 12 - half, Math.max(12 + half, left));
    }
    setPos({
      top: preferBelow ? r.bottom + gap : r.top - gap,
      left,
      place: preferBelow ? 'below' : 'above',
    });
  }, []);

  useLayoutEffect(() => {
    if (!pos || !tipRef.current) return;
    const half = tipRef.current.offsetWidth / 2;
    const left = Math.min(window.innerWidth - 12 - half, Math.max(12 + half, pos.left));
    if (Math.abs(left - pos.left) > 1) setPos(p => (p ? { ...p, left } : p));
  }, [pos]);

  useEffect(() => {
    if (!pos) return undefined;
    const onMove = () => place(hostRef.current);
    window.addEventListener('scroll', onMove, true);
    window.addEventListener('resize', onMove);
    return () => {
      window.removeEventListener('scroll', onMove, true);
      window.removeEventListener('resize', onMove);
    };
  }, [!!pos, place]);

  if (!label) return children;

  return (
    <span
      ref={hostRef}
      className={`ui-tooltip-host ${className}`.trim()}
      onMouseEnter={(e) => place(e.currentTarget)}
      onMouseLeave={() => setPos(null)}
      onFocus={(e) => place(e.currentTarget)}
      onBlur={() => setPos(null)}
    >
      {children}
      {pos
        ? ReactDOM.createPortal(
            <span
              ref={tipRef}
              className={`ui-tooltip place-${pos.place}`}
              role="tooltip"
              style={{ top: pos.top, left: pos.left }}
            >
              {label}
            </span>,
            document.body
          )
        : null}
    </span>
  );
}

const APP_SELECT_CHIP_MAX = 14;

function normalizeAppSelectOptions(options) {
  return (options || []).map((opt, i) => {
    if (opt && typeof opt === 'object' && !Array.isArray(opt)) {
      return {
        value: opt.value,
        label: opt.label != null ? String(opt.label) : String(opt.value ?? ''),
        disabled: !!opt.disabled,
        key: opt.key != null ? String(opt.key) : `${String(opt.value ?? '')}-${i}`,
      };
    }
    return { value: opt, label: String(opt), disabled: false, key: `${String(opt)}-${i}` };
  });
}

function sameAppSelectValue(a, b) {
  return String(a ?? '') === String(b ?? '');
}

/** Nexora dropdown (chips) or combobox (layout="combo") — never use a native <select>. */
function AppSelect({
  label = '',
  value,
  options = [],
  onChange,
  onQuery,
  ariaLabel,
  variant = 'field',
  disabled = false,
  placeholder,
  className = '',
  emptyOption,
  multiple = false,
  showMenuTitle = true,
  menuWidth,
  layout = 'auto',
}) {
  const [open, setOpen] = useState(false);
  const [pos, setPos] = useState(null);
  const [query, setQuery] = useState('');
  const [queriedOpts, setQueriedOpts] = useState(null);
  const [querying, setQuerying] = useState(false);
  const [activeIdx, setActiveIdx] = useState(0);
  const btnRef = useRef(null);
  const menuRef = useRef(null);
  const queryTimer = useRef(null);
  const queryReq = useRef(0);
  const useCombo = layout === 'combo';
  const fieldPlaceholder = placeholder || (useCombo ? 'Pesquisar…' : 'Selecionar');
  const opts = useMemo(() => {
    const next = normalizeAppSelectOptions(options);
    if (emptyOption == null || emptyOption === false) return next;
    if (typeof emptyOption === 'object') {
      return [{ value: emptyOption.value ?? '', label: emptyOption.label ?? '—', disabled: false, key: 'empty' }, ...next];
    }
    return [{ value: '', label: String(emptyOption), disabled: false, key: 'empty' }, ...next];
  }, [options, emptyOption]);
  const selectedValues = multiple ? (Array.isArray(value) ? value : []) : [];
  const current = !multiple ? opts.find(o => sameAppSelectValue(o.value, value)) : null;
  const catalog = useMemo(() => {
    if (useCombo && typeof onQuery === 'function' && queriedOpts != null) {
      return normalizeAppSelectOptions(queriedOpts);
    }
    return opts;
  }, [useCombo, onQuery, queriedOpts, opts]);
  const visibleOpts = useMemo(() => {
    if (!useCombo) return catalog;
    if (typeof onQuery === 'function' && queriedOpts != null) return catalog;
    const qFold = foldFormLabel(query);
    const selectedFold = current ? foldFormLabel(current.label) : '';
    if (!qFold || qFold === selectedFold) return catalog;
    return catalog.filter(o => foldFormLabel(o.label).includes(qFold) || foldFormLabel(o.value).includes(qFold));
  }, [useCombo, onQuery, queriedOpts, catalog, query, current]);
  const display = multiple
    ? (selectedValues.length === 0 || selectedValues.length === opts.filter(o => o.value !== '').length
      ? (fieldPlaceholder || 'Todas')
      : selectedValues.map(v => opts.find(o => sameAppSelectValue(o.value, v))?.label || String(v)).join(' · '))
    : (current?.label || fieldPlaceholder);
  const comboText = open ? query : (current?.label || '');
  const useList = useCombo || layout === 'list'
    || (layout !== 'chips' && (opts.length > APP_SELECT_CHIP_MAX || opts.some(o => o.label.length > 22)));
  const menuTitle = label || ariaLabel || 'Opções';
  const showTitle = useCombo ? false : showMenuTitle;

  const computePos = (count) => {
    const r = btnRef.current?.getBoundingClientRect();
    if (!r) return null;
    const n = count != null ? count : (useCombo ? Math.max(visibleOpts.length, 1) : opts.length);
    const attach = useCombo || useList;
    const width = menuWidth || (attach ? Math.max(r.width, 168) : 268);
    const rowH = useCombo ? 44 : (useList ? 36 : 40);
    const chrome = useCombo ? 20 : (useList ? (showTitle ? 56 : 16) : 64);
    const estimated = Math.min(360, chrome + Math.max(n, 1) * rowH);
    const gap = attach ? -1 : 8;
    const below = r.bottom + gap;
    const spaceBelow = window.innerHeight - below - 12;
    const spaceAbove = r.top - 12;
    const flip = spaceBelow < Math.min(estimated, 140) && spaceAbove > spaceBelow;
    const top = flip
      ? Math.max(12, r.top - estimated - (attach ? -1 : 8))
      : below;
    let left = r.left;
    if (left + width > window.innerWidth - 12) left = r.right - width;
    left = Math.max(12, Math.min(left, window.innerWidth - width - 12));
    return { top, left, width, flip };
  };

  const place = () => {
    const next = computePos();
    if (next) setPos(next);
  };

  const runQuery = (text) => {
    if (typeof onQuery !== 'function') return;
    if (queryTimer.current) clearTimeout(queryTimer.current);
    const reqId = ++queryReq.current;
    setQuerying(true);
    queryTimer.current = setTimeout(async () => {
      try {
        const result = await onQuery(String(text || '').trim());
        if (reqId !== queryReq.current) return;
        setQueriedOpts(Array.isArray(result) ? result : []);
      } catch {
        if (reqId !== queryReq.current) return;
        setQueriedOpts([]);
      } finally {
        if (reqId === queryReq.current) setQuerying(false);
      }
    }, 180);
  };

  const closeMenu = () => {
    setOpen(false);
    setQuery(current?.label || '');
    setActiveIdx(0);
    if (queryTimer.current) clearTimeout(queryTimer.current);
  };

  useEffect(() => {
    if (!open) return undefined;
    place();
    const onDoc = (e) => {
      if (btnRef.current?.contains(e.target) || menuRef.current?.contains(e.target)) return;
      closeMenu();
    };
    const onKey = (e) => { if (e.key === 'Escape') closeMenu(); };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onKey);
    window.addEventListener('resize', place);
    window.addEventListener('scroll', place, true);
    return () => {
      document.removeEventListener('mousedown', onDoc);
      document.removeEventListener('keydown', onKey);
      window.removeEventListener('resize', place);
      window.removeEventListener('scroll', place, true);
    };
  }, [open, visibleOpts.length, useList, useCombo]);

  useEffect(() => {
    if (!open || !useCombo) return;
    setActiveIdx((idx) => {
      if (!visibleOpts.length) return 0;
      return Math.min(idx, visibleOpts.length - 1);
    });
  }, [open, useCombo, visibleOpts.length, query]);

  const pick = (opt) => {
    if (opt.disabled || disabled) return;
    if (multiple) {
      const exists = selectedValues.some(v => sameAppSelectValue(v, opt.value));
      const next = exists
        ? selectedValues.filter(v => !sameAppSelectValue(v, opt.value))
        : [...selectedValues, opt.value];
      onChange?.(next);
      return;
    }
    onChange?.(opt.value);
    setQuery(opt.label || '');
    setOpen(false);
  };

  const isOn = (opt) => multiple
    ? selectedValues.some(v => sameAppSelectValue(v, opt.value))
    : sameAppSelectValue(opt.value, value);

  const openMenu = () => {
    if (disabled) return;
    if (useCombo) {
      setQuery(current?.label || '');
      if (typeof onQuery === 'function') runQuery(current?.label || '');
    }
    const next = computePos();
    if (next) setPos(next);
    setOpen(true);
  };

  const onComboKeyDown = (e) => {
    if (e.key === 'Escape') {
      e.preventDefault();
      closeMenu();
      return;
    }
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      if (!open) { openMenu(); return; }
      setActiveIdx(i => Math.min((visibleOpts.length || 1) - 1, i + 1));
      return;
    }
    if (e.key === 'ArrowUp') {
      e.preventDefault();
      if (!open) { openMenu(); return; }
      setActiveIdx(i => Math.max(0, i - 1));
      return;
    }
    if (e.key === 'Enter') {
      e.preventDefault();
      const hit = visibleOpts[activeIdx] || visibleOpts[0];
      if (open && hit) pick(hit);
    }
  };

  const rootClass = [
    'app-select',
    `app-select-${variant}`,
    useCombo ? 'app-select-combo' : (useList ? 'app-select-listbox' : ''),
    open ? 'is-open' : '',
    pos?.flip ? 'is-flip' : '',
    disabled ? 'is-disabled' : '',
    className,
  ].filter(Boolean).join(' ');

  return (
    <div className={rootClass}>
      {label && (useCombo || variant === 'pill' || variant === 'compact')
        ? <span className="app-select-label">{label}</span>
        : null}
      {useCombo ? (
        <input
          ref={btnRef}
          type="text"
          className={`app-select-trigger${open ? ' is-open' : ''}${!current && !open ? ' app-select-placeholder' : ''}`}
          value={comboText}
          placeholder={fieldPlaceholder}
          disabled={disabled}
          autoComplete="off"
          spellCheck={false}
          role="combobox"
          aria-label={ariaLabel || label || fieldPlaceholder}
          aria-expanded={open}
          aria-autocomplete="list"
          aria-haspopup="listbox"
          onFocus={() => { if (!open) openMenu(); }}
          onClick={() => { if (!open) openMenu(); }}
          onChange={(e) => {
            if (disabled) return;
            const text = e.target.value;
            setQuery(text);
            if (!open) {
              const next = computePos();
              if (next) setPos(next);
              setOpen(true);
            }
            if (typeof onQuery === 'function') runQuery(text);
          }}
          onKeyDown={onComboKeyDown}
        />
      ) : (
        <button
          ref={btnRef}
          type="button"
          className={`app-select-trigger${open ? ' is-open' : ''}`}
          aria-label={ariaLabel || label || fieldPlaceholder}
          aria-expanded={open}
          aria-haspopup="listbox"
          disabled={disabled}
          onClick={() => {
            if (disabled) return;
            if (open) { setOpen(false); return; }
            openMenu();
          }}
        >
          <strong className={!current && !multiple ? 'app-select-placeholder' : undefined}>{display}</strong>
        </button>
      )}
      {open && pos && ReactDOM.createPortal(
        <aside
          ref={menuRef}
          className={`chart-filter-popover app-select-popover${useList ? ' is-list' : ''}${useCombo ? ' is-combo' : ''}${pos.flip ? ' is-flip' : ''}`}
          role="listbox"
          aria-label={ariaLabel || label || fieldPlaceholder}
          aria-multiselectable={multiple || undefined}
          style={{ left: pos.left, top: pos.top, width: pos.width }}
          onMouseDown={e => e.stopPropagation()}
          onClick={e => e.stopPropagation()}
        >
          {showTitle && (
            <div className="chart-filter-popover-header">
              <div className="chart-filter-popover-title">{menuTitle}</div>
            </div>
          )}
          <div className="chart-filter-popover-body">
            <div className={useCombo ? 'app-select-combo-list' : (useList ? 'app-select-list' : 'checkbox-group-chips')}>
              {useCombo && querying && (
                <div className="app-select-combo-empty">A pesquisar…</div>
              )}
              {useCombo && !querying && visibleOpts.length === 0 && (
                <div className="app-select-combo-empty">Sem resultados</div>
              )}
              {(!useCombo || !querying) && (useCombo ? visibleOpts : opts).map((opt, i) => (
                <button
                  key={opt.key}
                  type="button"
                  role="option"
                  aria-selected={isOn(opt)}
                  disabled={opt.disabled}
                  className={useCombo
                    ? `app-select-combo-opt${isOn(opt) ? ' is-on' : ''}${i === activeIdx ? ' is-active' : ''}`
                    : `avaliacao-check app-select-opt${isOn(opt) ? ' is-on' : ''}${useList ? ' is-row' : ''}`}
                  onMouseDown={(e) => { if (useCombo) e.preventDefault(); }}
                  onMouseEnter={() => { if (useCombo) setActiveIdx(i); }}
                  onClick={() => pick(opt)}
                >
                  {opt.label}
                </button>
              ))}
            </div>
          </div>
        </aside>,
        document.body
      )}
    </div>
  );
}

const INSERIR_LINKS = [
  { id: 'faturacao', label: 'Pedido de faturação', href: '' },
  { id: 'marketing', label: 'Pedido marketing', href: '' },
  { id: 'fotografia', label: 'Pedido fotografia', href: '' },
];

function InserirMenu({ onPipeline }) {
  const [open, setOpen] = useState(false);
  const rootRef = useRef(null);
  useEffect(() => {
    if (!open) return undefined;
    const onDoc = (e) => {
      if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
    };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [open]);

  return (
    <div className="inserir-menu" ref={rootRef}>
      <button type="button" className="btn-primary-accent" onClick={() => setOpen(v => !v)} aria-expanded={open}>
        <Icon name="plus" size={16} color="#FFFFFF" />
        <span className="btn-label-text">Inserir</span>
      </button>
      {open && (
        <div className="inserir-panel" role="menu">
          <button type="button" className="inserir-item" role="menuitem" onClick={() => { setOpen(false); onPipeline?.(); }}>
            Pipeline
          </button>
          {INSERIR_LINKS.map(link => (
            <a
              key={link.id}
              className={`inserir-item${link.href ? '' : ' is-pending'}`}
              role="menuitem"
              href={link.href || undefined}
              target={link.href ? '_blank' : undefined}
              rel={link.href ? 'noreferrer' : undefined}
              title={link.href ? 'Abre numa nova janela' : 'Ligação da Power App ainda por configurar'}
              onClick={(e) => { if (!link.href) e.preventDefault(); else setOpen(false); }}
            >
              <span>{link.label}</span>
              <Icon name="external" size={12} />
            </a>
          ))}
        </div>
      )}
    </div>
  );
}

function Icon({ name, size = 20, color, className, title }) {
  const FA_ICONS = {
    bank: 'fa-solid fa-building-columns',
    edit: 'fa-solid fa-pen',
    graph: 'fa-solid fa-chart-column',
    chartHbar: 'fa-solid fa-bars-staggered',
    chartPie: 'fa-solid fa-chart-pie',
    chartProgress: 'fa-solid fa-bars-progress',
    home: 'fa-solid fa-house',
    team: 'fa-solid fa-people-group',
    trash: 'fa-solid fa-trash',
    user: 'fa-solid fa-user',
    mediacao: 'fa-solid fa-house-chimney',
    angariacao: 'fa-solid fa-bullhorn',
    soi: 'fa-solid fa-bullseye',
    investimentos: 'fa-solid fa-briefcase',
    eye: 'fa-solid fa-eye',
    sliders: 'fa-solid fa-sliders',
    check: 'fa-solid fa-check',
    settings: 'fa-solid fa-gear',
    logout: 'fa-solid fa-right-from-bracket',
    chevronLeft: 'fa-solid fa-chevron-left',
    chevronRight: 'fa-solid fa-chevron-right',
    chevronUp: 'fa-solid fa-chevron-up',
    chevronDown: 'fa-solid fa-chevron-down',
    arrowLeft: 'fa-solid fa-arrow-left',
    arrowRight: 'fa-solid fa-arrow-right',
    refresh: 'fa-solid fa-rotate-right',
    filter: 'fa-solid fa-filter',
    filterClear: 'fa-solid fa-filter-circle-xmark',
    bars: 'fa-solid fa-bars',
    sortAsc: 'fa-solid fa-arrow-down-a-z',
    sortDesc: 'fa-solid fa-arrow-up-z-a',
    sortClear: 'fa-solid fa-xmark',
    search: 'fa-solid fa-magnifying-glass',
    close: 'fa-solid fa-xmark',
    checkmark: 'fa-solid fa-circle-check',
    sharepoint: 'fa-brands fa-microsoft',
    document: 'fa-solid fa-file-lines',
    camera: 'fa-solid fa-camera',
    message: 'fa-solid fa-comment-dots',
    bell: 'fa-solid fa-bell',
    calendar: 'fa-solid fa-calendar',
    external: 'fa-solid fa-arrow-up-right-from-square',
    trophy: 'fa-solid fa-trophy',
    star: 'fa-solid fa-star',
    ranking: 'fa-solid fa-chart-simple',
    kanban: 'fa-solid fa-table-columns',
    plus: 'fa-solid fa-plus',
    minus: 'fa-solid fa-minus',
    thumb: 'fa-solid fa-thumbs-up',
    comment: 'fa-solid fa-comment',
    share: 'fa-solid fa-share',
    clock: 'fa-solid fa-clock',
    activity: 'fa-solid fa-clock',
    layout: 'fa-solid fa-table-cells-large',
    clipboardCheck: 'fa-solid fa-clipboard-check',
    clipboardList: 'fa-solid fa-clipboard-list',
    phone: 'fa-solid fa-phone',
    envelope: 'fa-solid fa-envelope',
    location: 'fa-solid fa-location-dot',
    lock: 'fa-solid fa-lock',
    circleXmark: 'fa-solid fa-circle-xmark',
    triangleExclamation: 'fa-solid fa-triangle-exclamation',
    circleInfo: 'fa-solid fa-circle-info',
    circleQuestion: 'fa-solid fa-circle-question',
    palette: 'fa-solid fa-palette',
  };

  const faClass = FA_ICONS[name];
  if (faClass) {
    return (
      <span
        className={`ui-icon ${className || ''}`.trim()}
        style={{ width: size, height: size, color: color || 'currentColor', fontSize: size }}
        role={title ? 'img' : 'presentation'}
        aria-label={title || undefined}
        aria-hidden={title ? undefined : true}
      >
        <i className={faClass} aria-hidden="true" />
      </span>
    );
  }

  const theme = window.MyHabitaTheme;
  if (!theme?.getIcon || !name) return null;
  const html = theme.getIcon(name, color);
  if (!html) return null;
  return (
    <span
      className={`ui-icon ${className || ''}`.trim()}
      style={{ width: size, height: size }}
      role={title ? 'img' : 'presentation'}
      aria-label={title || undefined}
      aria-hidden={title ? undefined : true}
      dangerouslySetInnerHTML={{ __html: html }}
    />
  );
}

function appAssets() {
  return window.MyHabitaTheme?.assets ?? {};
}

/** Viewport breakpoints — keep in sync with CSS --bp-phone / --bp-tablet. */
const BP_PHONE_MAX = 720;
const BP_TABLET_MAX = 1199;
const PHONE_LIST_VIEW_KEY = 'myhabita.phoneListView';
const LIST_LAYOUT_KEY_PREFIX = 'myhabita.listLayout.';

const ViewportNavContext = React.createContext({
  mode: 'pc',
  isPhone: false,
  isTablet: false,
  isPc: true,
  navOpen: false,
  openNav: () => {},
  closeNav: () => {},
});

function useViewportMode() {
  const [mode, setMode] = useState(() => {
    if (typeof window === 'undefined') return 'pc';
    const w = window.innerWidth;
    if (w <= BP_PHONE_MAX) return 'phone';
    if (w <= BP_TABLET_MAX) return 'tablet';
    return 'pc';
  });

  useEffect(() => {
    if (typeof window === 'undefined' || !window.matchMedia) return undefined;
    const phoneMq = window.matchMedia(`(max-width: ${BP_PHONE_MAX}px)`);
    const tabletMq = window.matchMedia(`(max-width: ${BP_TABLET_MAX}px)`);
    const sync = () => {
      if (phoneMq.matches) setMode('phone');
      else if (tabletMq.matches) setMode('tablet');
      else setMode('pc');
    };
    sync();
    const add = (mq, fn) => {
      if (mq.addEventListener) mq.addEventListener('change', fn);
      else if (mq.addListener) mq.addListener(fn);
    };
    const remove = (mq, fn) => {
      if (mq.removeEventListener) mq.removeEventListener('change', fn);
      else if (mq.removeListener) mq.removeListener(fn);
    };
    add(phoneMq, sync);
    add(tabletMq, sync);
    return () => {
      remove(phoneMq, sync);
      remove(tabletMq, sync);
    };
  }, []);

  return mode;
}

function useViewportNav() {
  return React.useContext(ViewportNavContext);
}

function loadPhoneListView() {
  try {
    const v = window.localStorage.getItem(PHONE_LIST_VIEW_KEY);
    return v === 'table' ? 'table' : 'cards';
  } catch {
    return 'cards';
  }
}

function savePhoneListView(view) {
  try {
    window.localStorage.setItem(PHONE_LIST_VIEW_KEY, view === 'table' ? 'table' : 'cards');
  } catch { /* ignore */ }
}

function usePhoneListView() {
  const [view, setViewState] = useState(loadPhoneListView);
  const setView = useCallback((next) => {
    const v = next === 'table' ? 'table' : 'cards';
    setViewState(v);
    savePhoneListView(v);
  }, []);
  return [view, setView];
}

function loadListLayoutView(storageKey, fallback = 'table') {
  try {
    const per = window.localStorage.getItem(LIST_LAYOUT_KEY_PREFIX + storageKey);
    if (per === 'cards' || per === 'table') return per;
  } catch { /* ignore */ }
  return fallback === 'cards' ? 'cards' : 'table';
}

function saveListLayoutView(storageKey, view) {
  try {
    window.localStorage.setItem(LIST_LAYOUT_KEY_PREFIX + storageKey, view === 'cards' ? 'cards' : 'table');
  } catch { /* ignore */ }
}

function useListLayoutView(storageKey, defaultView = 'table') {
  const key = String(storageKey || 'default');
  const fallback = defaultView === 'cards' ? 'cards' : 'table';
  const [view, setViewState] = useState(() => loadListLayoutView(key, fallback));
  useEffect(() => {
    setViewState(loadListLayoutView(key, fallback));
  }, [key, fallback]);
  const setView = useCallback((next) => {
    const v = next === 'table' ? 'table' : 'cards';
    setViewState(v);
    saveListLayoutView(key, v);
  }, [key]);
  return [view, setView];
}

const MATCHING_SPLIT_KEY_PREFIX = 'myhabita.split.';
const MATCHING_SPLIT_MIN = 22;
const MATCHING_SPLIT_MAX = 62;

function clampMatchingSplitPct(pct) {
  const n = Number(pct);
  if (!Number.isFinite(n)) return 30;
  return Math.min(MATCHING_SPLIT_MAX, Math.max(MATCHING_SPLIT_MIN, n));
}

function loadMatchingSplitPct(storageKey, fallback = 30) {
  try {
    const raw = window.localStorage.getItem(MATCHING_SPLIT_KEY_PREFIX + storageKey);
    if (raw == null || raw === '') return fallback;
    return clampMatchingSplitPct(raw);
  } catch {
    return fallback;
  }
}

function saveMatchingSplitPct(storageKey, pct) {
  try {
    window.localStorage.setItem(MATCHING_SPLIT_KEY_PREFIX + storageKey, String(Math.round(pct * 10) / 10));
  } catch { /* ignore */ }
}

function MatchingSplitShell({
  storageKey,
  defaultLeftPct = 30,
  className = '',
  children,
}) {
  const viewportMode = useViewportMode();
  const stacked = viewportMode !== 'pc';
  const fallback = clampMatchingSplitPct(defaultLeftPct);
  const [leftPct, setLeftPct] = useState(() => loadMatchingSplitPct(storageKey, fallback));
  const [resizing, setResizing] = useState(false);
  const splitRef = useRef(null);
  const dragRef = useRef(null);
  const pctRef = useRef(leftPct);
  pctRef.current = leftPct;

  useEffect(() => {
    setLeftPct(loadMatchingSplitPct(storageKey, fallback));
  }, [storageKey, fallback]);

  const commitPct = useCallback((next) => {
    const clamped = clampMatchingSplitPct(next);
    setLeftPct(clamped);
    saveMatchingSplitPct(storageKey, clamped);
    return clamped;
  }, [storageKey]);

  const onPointerDown = useCallback((e) => {
    if (stacked) return;
    if (e.button != null && e.button !== 0) return;
    e.preventDefault();
    const el = splitRef.current;
    if (!el) return;
    const width = el.getBoundingClientRect().width || 1;
    dragRef.current = { startX: e.clientX, startPct: pctRef.current, width };
    setResizing(true);
    try { e.currentTarget.setPointerCapture(e.pointerId); } catch { /* ignore */ }
  }, [stacked]);

  const onPointerMove = useCallback((e) => {
    const drag = dragRef.current;
    if (!drag) return;
    const next = drag.startPct + ((e.clientX - drag.startX) / drag.width) * 100;
    setLeftPct(clampMatchingSplitPct(next));
  }, []);

  const onPointerUp = useCallback(() => {
    if (!dragRef.current) return;
    dragRef.current = null;
    setResizing(false);
    saveMatchingSplitPct(storageKey, pctRef.current);
  }, [storageKey]);

  const onKeyDown = useCallback((e) => {
    if (stacked) return;
    if (e.key === 'ArrowLeft') {
      e.preventDefault();
      commitPct(pctRef.current - 2);
    } else if (e.key === 'ArrowRight') {
      e.preventDefault();
      commitPct(pctRef.current + 2);
    } else if (e.key === 'Home') {
      e.preventDefault();
      commitPct(fallback);
    }
  }, [stacked, commitPct, fallback]);

  const panes = React.Children.toArray(children);
  return (
    <div
      ref={splitRef}
      className={`matching-split matching-split-resizable${resizing ? ' is-resizing' : ''}${className ? ` ${className}` : ''}`}
      style={stacked ? undefined : { gridTemplateColumns: `minmax(220px, ${leftPct}%) 8px minmax(280px, 1fr)` }}
    >
      {panes[0]}
      {!stacked && (
        <div
          className="matching-split-handle"
          role="separator"
          aria-orientation="vertical"
          aria-label="Redimensionar colunas"
          aria-valuemin={MATCHING_SPLIT_MIN}
          aria-valuemax={MATCHING_SPLIT_MAX}
          aria-valuenow={Math.round(leftPct)}
          tabIndex={0}
          onPointerDown={onPointerDown}
          onPointerMove={onPointerMove}
          onPointerUp={onPointerUp}
          onPointerCancel={onPointerUp}
          onDoubleClick={() => commitPct(fallback)}
          onKeyDown={onKeyDown}
        />
      )}
      {panes[1]}
    </div>
  );
}

function friendlyTitleLabel(listKey) {
  switch (String(listKey || '')) {
    case 'oportunidades': return 'Oportunidade';
    case 'ativos': return 'Potencial';
    case 'clientes': return 'Cliente';
    case 'pipeline': return 'Negócio';
    default: return 'Identificação';
  }
}

function applyFriendlyColumnLabels(columns, listKey) {
  return toColumnDefs(columns).map((col) => {
    const key = String(col.key || '');
    const label = String(col.label || '').trim();
    if (/^title$/i.test(key) && (!label || /^title$/i.test(label))) {
      return { ...col, label: friendlyTitleLabel(listKey) };
    }
    return col;
  });
}

const CARD_FIELD_PRIORITY = [
  'consultor', 'estado', 'tipo', 'local', 'operacao', 'valor', 'data', 'cliente',
  'imovel', 'potencial', 'natureza', 'liquidado', 'comissao', 'probabilidade',
  'email', 'contacto', 'referencia', 'validado', 'validacao',
];

const LIST_CARD_SLOT_HINTS = {
  ativos: ['consultor', 'tipo', 'local', 'operacao', 'valor', 'estado', 'area', 'potencial'],
  oportunidades: ['consultor', 'tipo', 'local', 'operacao', 'valor', 'estado', 'potencial', 'cliente', 'data', 'referencia'],
  clientes: ['consultor', 'tipo', 'email', 'contacto', 'data', 'referencia'],
};

function columnKeyScore(key, hints = CARD_FIELD_PRIORITY) {
  const k = String(key || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
  const idx = hints.findIndex(p => k === p || k.includes(p));
  return idx === -1 ? 100 : idx;
}

function columnSearchBlob(col) {
  return `${col?.key || ''} ${col?.label || ''}`
    .toLowerCase()
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '');
}

function pickCardPrimaryColumn(columns, listKey) {
  const defs = toColumnDefs(columns).filter(c => c.visible !== false);
  if (!defs.length) return null;
  const scored = defs.map((c, i) => {
    const blob = columnSearchBlob(c);
    const k = String(c.key || '').toLowerCase();
    let score = 50 + i;
    if (listKey === 'ativos') {
      if (/denomina/.test(blob)) score = 0;
      else if (/identifica/.test(blob) && !/potencial/.test(blob)) score = 1;
      else if (k === 'title' || k === 'linktitle') score = 25;
    } else if (k === 'title' || k === 'linktitle' || k === 'assunto' || k === 'negocio' || /identifica/.test(blob)) {
      score = 0;
    }
    return { c, score };
  });
  scored.sort((a, b) => a.score - b.score);
  return scored[0].c;
}

function pickCardSecondaryColumns(columns, primaryKey, max = 4, listKey = null) {
  const defs = toColumnDefs(columns).filter(c => c.visible !== false && c.key !== primaryKey);
  const hints = LIST_CARD_SLOT_HINTS[listKey] || CARD_FIELD_PRIORITY;
  const consultor = defs.find(c => /consultor/.test(columnSearchBlob(c)));
  const rest = defs
    .filter(c => c !== consultor)
    .sort((a, b) => columnKeyScore(a.key, hints) - columnKeyScore(b.key, hints) || (a.order ?? 0) - (b.order ?? 0));
  const out = [];
  if (consultor) out.push(consultor);
  rest.forEach((c) => {
    if (out.length < max) out.push(c);
  });
  return out;
}

function slimColumnsForPhone(columns, maxVisible = 5) {
  const defs = toColumnDefs(columns);
  const visible = defs.filter(c => c.visible !== false);
  if (visible.length <= maxVisible) return defs;
  const primary = pickCardPrimaryColumn(defs);
  const keep = new Set();
  if (primary) keep.add(primary.key);
  pickCardSecondaryColumns(defs, primary?.key, maxVisible - 1).forEach(c => keep.add(c.key));
  if (keep.size < maxVisible) {
    visible.forEach((c) => {
      if (keep.size < maxVisible) keep.add(c.key);
    });
  }
  return defs.map(c => ({ ...c, visible: keep.has(c.key) }));
}

function emptyPhoneCardLayout() {
  return { title: '', slot1: '', slot2: '', slot3: '', slot4: '' };
}

function normalizePhoneCardLayout(raw) {
  if (!raw) return emptyPhoneCardLayout();
  return {
    title: String(raw.title || raw.Title || ''),
    slot1: String(raw.slot1 || raw.Slot1 || ''),
    slot2: String(raw.slot2 || raw.Slot2 || ''),
    slot3: String(raw.slot3 || raw.Slot3 || ''),
    slot4: String(raw.slot4 || raw.Slot4 || ''),
  };
}

function phoneCardLayoutIsEmpty(layout) {
  const n = normalizePhoneCardLayout(layout);
  return !n.title && !n.slot1 && !n.slot2 && !n.slot3 && !n.slot4;
}

function findColumnDefByKey(columns, key) {
  if (!key) return null;
  const k = String(key).toLowerCase();
  return toColumnDefs(columns).find(c => String(c.key).toLowerCase() === k) || null;
}

function resolvePhoneCardFields(columns, cardLayout, listKey = null) {
  const defs = toColumnDefs(columns);
  const layout = normalizePhoneCardLayout(cardLayout);
  if (phoneCardLayoutIsEmpty(layout)) {
    const title = pickCardPrimaryColumn(defs, listKey);
    const slots = pickCardSecondaryColumns(defs, title?.key, 4, listKey);
    return { title, slots, auto: true };
  }
  return {
    title: findColumnDefByKey(defs, layout.title),
    slots: [layout.slot1, layout.slot2, layout.slot3, layout.slot4]
      .map(k => findColumnDefByKey(defs, k))
      .filter(Boolean),
    auto: false,
  };
}

function formatDataListCardCell(row, col) {
  if (!col || !row) return '';
  const raw = getFieldValue(row, col.key) ?? row[col.key];
  const text = formatCellValue(raw, col.key, col.format);
  if (text == null || text === '') return '';
  return String(text);
}

function isDataListCardStatusColumn(col) {
  return /estado|validado|validacao|natureza/i.test(String(col?.key || ''));
}

function PhoneListViewToggle({ view, onChange }) {
  return (
    <div className="phone-list-view-toggle" role="group" aria-label="Vista da lista">
      <AppTooltip label="Cartões" className="list-view-tip">
        <button
          type="button"
          className={`toolbar-icon-btn phone-view-btn${view === 'cards' ? ' active' : ''}`}
          aria-label="Cartões"
          aria-pressed={view === 'cards'}
          onClick={() => onChange('cards')}
        >
          <Icon name="layout" size={15} />
        </button>
      </AppTooltip>
      <AppTooltip label="Tabela" className="list-view-tip">
        <button
          type="button"
          className={`toolbar-icon-btn phone-view-btn${view === 'table' ? ' active' : ''}`}
          aria-label="Tabela"
          aria-pressed={view === 'table'}
          onClick={() => onChange('table')}
        >
          <Icon name="kanban" size={15} />
        </button>
      </AppTooltip>
    </div>
  );
}

function NavMenuButton() {
  const { isPhone, openNav } = useViewportNav();
  if (!isPhone) return null;
  return (
    <button
      type="button"
      className="btn-icon-ghost nav-menu-btn"
      onClick={openNav}
      aria-label="Abrir menu"
      title="Menu"
    >
      <Icon name="bars" size={20} />
    </button>
  );
}

function DataListCards({
  columns,
  rows,
  selectedId = null,
  onRowClick = null,
  onRowDoubleClick = null,
  renderCardActions = null,
  emptyMessage = 'Sem registos.',
  cardLayout = null,
  listKey = null,
}) {
  const { title: titleCol, slots } = useMemo(
    () => resolvePhoneCardFields(columns, cardLayout, listKey),
    [columns, cardLayout, listKey]
  );

  if (!rows?.length) {
    return <div className="data-list-cards-empty">{emptyMessage}</div>;
  }

  return (
    <div className="data-list-cards" role="list">
      {rows.map((row) => {
        const id = row.id ?? row.Id;
        const selected = selectedId != null && String(selectedId) === String(id);
        let titleText = titleCol ? formatDataListCardCell(row, titleCol) : String(id ?? '—');
        if (!titleText || titleText === '—') {
          const fallback = pickCardPrimaryColumn(
            toColumnDefs(columns).filter(c => c.key !== titleCol?.key),
            listKey
          );
          if (fallback) titleText = formatDataListCardCell(row, fallback) || titleText;
        }
        return (
          <article
            key={id ?? titleText}
            role="listitem"
            className={`data-list-card${selected ? ' is-selected' : ''}`}
            tabIndex={0}
            onClick={() => onRowClick?.(row)}
            onDoubleClick={() => onRowDoubleClick?.(row)}
            onKeyDown={(e) => {
              if (e.key === 'Enter' || e.key === ' ') {
                e.preventDefault();
                onRowClick?.(row);
              }
            }}
          >
            <div className="data-list-card-body">
              <h4 className="data-list-card-title">{titleText || '—'}</h4>
              {slots.length > 0 && (
                <dl className="data-list-card-meta">
                  {slots.map((col) => {
                    const text = formatDataListCardCell(row, col);
                    if (!text || text === '—') return null;
                    return (
                      <div key={col.key} className="data-list-card-meta-row">
                        <dt>{col.label || col.key}</dt>
                        <dd>
                          {isDataListCardStatusColumn(col) ? (
                            <span className={`status-badge ${statusClass(text)}`}>{text}</span>
                          ) : text}
                        </dd>
                      </div>
                    );
                  })}
                </dl>
              )}
            </div>
            {typeof renderCardActions === 'function' && (
              <div className="data-list-card-actions" onClick={(e) => e.stopPropagation()}>
                {renderCardActions(row)}
              </div>
            )}
          </article>
        );
      })}
    </div>
  );
}

function LoadingSpinner({ message = 'A carregar...', white = false, fullScreen = false }) {
  const assets = appAssets();
  const src = white ? assets.loadingWhite : assets.loading;
  return (
    <div className={`loading-spinner ${fullScreen ? 'loading-spinner-full' : ''}`} role="status" aria-live="polite">
      {src ? <img src={src} alt="" className="loading-spinner-gif" /> : null}
      {message ? <p className="loading-spinner-text">{message}</p> : null}
    </div>
  );
}

function BrandLogo({ variant = 'myhabita', collapsed = false, className = '' }) {
  const assets = appAssets();
  const srcMap = {
    myhabita: assets.logoMyHabita,
    icon: assets.logoIcon || assets.logoMyHabita,
    habita: assets.logoHabita,
    habitaWhite: assets.logoHabitaWhite,
    himo: assets.logoHimo,
  };

  const useIcon = variant === 'myhabita' && collapsed;
  const src = useIcon ? srcMap.icon : (srcMap[variant] || srcMap.myhabita);
  if (!src) return null;

  const alt = variant === 'himo'
    ? 'Grupo HIMO'
    : variant === 'habita' || variant === 'habitaWhite'
      ? 'Habita'
      : 'myHABITA';

  return (
    <img
      src={src}
      alt={alt}
      className={`brand-logo brand-logo-${variant} ${className}`.trim()}
    />
  );
}

const NAV_ITEMS = [
  { id: 'dashboard', label: 'Dashboard', module: 'dashboard', icon: 'graph' },
  { id: 'equipa', label: 'Equipa', module: 'equipa', icon: 'team' },
  { id: 'calendario', label: 'Calendário', module: 'calendario', icon: 'calendar' },
  { id: 'mediacao', label: 'Mediação', module: 'mediacao', icon: 'mediacao' },
  { id: 'intermediacao', label: 'Intermediação', module: 'intermediacao', icon: 'bank' },
  { id: 'carteira', label: 'Investimentos', module: 'carteira', icon: 'investimentos' },
  { id: 'angariacao', label: 'Angariação', module: 'angariacao', icon: 'angariacao' },
  { id: 'soi', label: 'SOI', module: 'soi', icon: 'soi' },
];

function isNavItemEnabled(item, enabledModules) {
  if (item.id === 'dashboard') {
    return NAV_ITEMS
      .filter(n => n.id !== 'dashboard' && n.id !== 'equipa')
      .some(n => enabledModules.includes(n.module));
  }
  if (item.id === 'equipa') return enabledModules.includes('equipa');
  if (item.id === 'calendario') return true;
  return enabledModules.includes(item.module);
}

const PAGE_IDS = NAV_ITEMS.map(item => item.id);

const ROUTE_DEFAULT_TABS = {
  equipa: 'main',
  mediacao: 'negocios',
  carteira: 'carteira',
};

const CARTEIRA_SLUG_TO_ID = {
  carteira: 'ativos',
  clientes: 'clientes',
  oportunidades: 'oportunidades',
  matching: 'matching',
};

const CARTEIRA_ID_TO_SLUG = Object.fromEntries(
  Object.entries(CARTEIRA_SLUG_TO_ID).map(([slug, id]) => [id, slug])
);

const INV_TIPO_ATIVO = [
  'Residencial', 'Escritórios', 'Comercial', 'Logística',
  'Hotelaria', 'Equipamentos coletivos', 'Serviços',
];
const INV_POTENCIAL = [
  'Reabilitação', 'Nova Construção', 'Promoção', 'Built to Rent', 'Rendimento',
  'Habitação', 'Comercial/Serviços', 'Logística', 'Hotelaria',
  'Empreendimento turístico', 'Sem projeto', 'PIP', 'Licenciamento',
];
const INV_TIPO_CLIENTE = ['Particular', 'Empresa', 'Fundo', 'Family Office', 'Outro'];
const INV_ESTADO_ATIVO = ['Disponível', 'Off-Market', 'Reservado', 'Vendido', 'Retirado'];
const INV_ESTADO_OPP = ['Ativa', 'Ganha', 'Perdida'];
const INV_OPERACAO = ['Venda', 'Arrendamento', 'Trespasse'];
const INV_LOCALIZACAO_FALLBACK = ['Lisboa', 'Porto', 'Alentejo', 'Sines', 'Oeste', 'Algarve'];
const INV_MULTI_CHOICE_KEYS = new Set([
  'PotencialAtivo', 'PotencialOportunidade', 'PotencialdaOportunidade',
  'TipoAtivoMatch', 'TipodeAtivo', 'TipoAtivo', 'LocalizacaoMatch',
]);

/** New schema names ↔ original SharePoint internal names (same value, two columns). */
const INV_FIELD_ALIAS_GROUPS = [
  ['TipoAtivoMatch', 'TipodeAtivo', 'TipoAtivo'],
  ['LocalizacaoMatch', 'Localiza_x00e7__x00e3_o', 'Localizacao'],
  ['AreaBrutaM2', 'field_9'],
  ['ValorEuro', 'field_8', 'Valor'],
  ['AreaBrutaMin', 'field_10'],
  ['AreaBrutaMax', 'AreabrutaMax_x0028_m2_x0029_'],
  ['ValorMinEuro', 'field_8'],
  ['ValorMaxEuro', 'ValorMax_x0028__x20ac__x0029_'],
  ['RendimentoMinPct', 'RendimentoMin_x0028__x0025__x002'],
  ['PotencialOportunidade', 'PotencialdaOportunidade'],
  ['PotencialAtivo'],
];

function invFieldAliasKeys(fieldKey) {
  const k = String(fieldKey || '').toLowerCase();
  if (!k) return [];
  const group = INV_FIELD_ALIAS_GROUPS.find(g => g.some(n => n.toLowerCase() === k));
  return group ? group.filter(n => n.toLowerCase() !== k) : [];
}

function foldFormLabel(value) {
  return String(value || '')
    .toLowerCase()
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '');
}

function parseMaybeJson(value) {
  if (value == null || value === '') return value;
  if (typeof value !== 'string') return value;
  const t = value.trim();
  if (!(t.startsWith('{') || t.startsWith('['))) return value;
  try { return JSON.parse(t); } catch { return value; }
}

function isSpLocationValue(value) {
  const v = parseMaybeJson(value);
  if (!v || typeof v !== 'object' || Array.isArray(v)) return false;
  const hasName = v.DisplayName != null || v.displayName != null;
  const hasLocBits = Object.prototype.hasOwnProperty.call(v, 'LocationUri')
    || Object.prototype.hasOwnProperty.call(v, 'locationUri')
    || v.Coordinates != null || v.coordinates != null
    || v.Address != null || v.address != null
    || v.EntityType != null || v.entityType != null;
  return hasName && hasLocBits;
}

function locationDisplayName(value) {
  const v = parseMaybeJson(value);
  if (v == null || v === '') return '';
  if (Array.isArray(v)) {
    return v.map((x) => (typeof x === 'object' && x ? (x.DisplayName || x.displayName || '') : String(x || ''))).filter(Boolean).join(', ');
  }
  if (typeof v === 'string') return v;
  if (typeof v !== 'object') return String(v);
  const city = v.Address?.City || v.Address?.city || v.address?.City || v.address?.city;
  return String(v.DisplayName || v.displayName || city || '').trim();
}

function buildLocationValue(text, previous) {
  const name = String(text || '').trim();
  if (!name) return '';
  const prev = previous && typeof previous === 'object' && !Array.isArray(previous)
    ? previous
    : (isSpLocationValue(previous) ? parseMaybeJson(previous) : null);
  if (!prev || typeof prev !== 'object') return { DisplayName: name, EntityType: 'Custom' };
  return {
    ...prev,
    DisplayName: name,
    EntityType: prev.EntityType || prev.entityType || 'Custom',
  };
}

function toLocationPayload(value) {
  if (value == null || value === '') return null;
  const parsed = parseMaybeJson(value);
  if (typeof parsed === 'string') {
    const name = parsed.trim();
    return name ? { DisplayName: name, EntityType: 'Custom' } : null;
  }
  if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
    const name = locationDisplayName(parsed);
    if (!name) return null;
    return {
      ...parsed,
      DisplayName: name,
      EntityType: parsed.EntityType || parsed.entityType || 'Custom',
    };
  }
  return null;
}

function isCityMultiLocationKey(key) {
  return String(key || '') === 'LocalizacaoMatch';
}

function isPotencialFormField(key, label) {
  const k = String(key || '');
  if (/^potencial/i.test(k)) return true;
  return foldFormLabel(label).includes('potencial');
}

function isTipoAtivoMultiField(key, label) {
  const k = String(key || '');
  if (k === 'TipoAtivoMatch' || k === 'TipodeAtivo' || k === 'TipoAtivo') return true;
  const lb = foldFormLabel(label);
  return lb.includes('tipo') && lb.includes('ativo');
}

function formColumnKind(colMeta) {
  return String(colMeta?.columnKind || colMeta?.ColumnKind || colMeta?.type || colMeta?.Type || '').toLowerCase();
}

function spColName(col) {
  return String(col?.name || col?.Name || '').trim();
}

function spColIsWritable(col) {
  if (!col) return false;
  if (col.readOnly === true || col.ReadOnly === true) return false;
  const name = spColName(col);
  if ((col.isSystem === true || col.IsSystem === true) && name.toLowerCase() !== 'title') return false;
  if (col.calculatedFormula || col.CalculatedFormula) return false;
  const kind = formColumnKind(col);
  if (kind === 'calculated') return false;
  return true;
}

function asSpColumnList(res) {
  if (Array.isArray(res)) return res;
  if (Array.isArray(res?.columns)) return res.columns;
  if (Array.isArray(res?.Columns)) return res.Columns;
  return [];
}

const INV_WRITE_WITHOUT_META = new Set([
  'Title',
  'Opera_x00e7__x00e3_o',
  'Estado',
  'TipoAtivoMatch',
  'LocalizacaoMatch',
  'PotencialOportunidade',
  'PotencialAtivo',
  'AreaBrutaMin',
  'AreaBrutaMax',
  'ValorMinEuro',
  'RendimentoMinPct',
  'AreaBrutaM2',
  'ValorEuro',
  'TipoCliente',
  'Email',
  'Contacto',
]);

function spColIsLocation(col) {
  const kind = formColumnKind(col);
  return kind === 'location' || kind === 'geolocation';
}

function findSpColByNames(cols, names) {
  const list = Array.isArray(cols) ? cols : [];
  for (const name of names || []) {
    const wanted = String(name || '').toLowerCase();
    if (!wanted) continue;
    const col = list.find(c => spColName(c).toLowerCase() === wanted);
    if (col) return col;
  }
  return null;
}

function invWriteValue(col, value, preferArray) {
  const key = spColName(col);
  const kind = formColumnKind(col);
  const allowMultiple = col?.allowMultiple === true || col?.AllowMultiple === true || kind === 'multichoice';
  const wantsArray = allowMultiple || /Match$/i.test(key);
  if (wantsArray) return Array.isArray(value) ? value.filter(v => v != null && v !== '') : [value];
  return Array.isArray(value) ? value[0] : value;
}

function setWritableField(target, cols, names, value, preferArray = false) {
  if (value == null || value === '') return false;
  if (Array.isArray(value) && !value.length) return false;
  const list = Array.isArray(cols) ? cols : [];
  if (!list.length) {
    const key = String(names?.[0] || '').trim();
    if (!key || !INV_WRITE_WITHOUT_META.has(key)) return false;
    target[key] = preferArray || /Match$/i.test(key)
      ? (Array.isArray(value) ? value.filter(v => v != null && v !== '') : [value])
      : (Array.isArray(value) ? value[0] : value);
    return true;
  }
  const col = findSpColByNames(list, names);
  if (!col || !spColIsWritable(col) || spColIsLocation(col)) return false;
  const kind = formColumnKind(col);
  if (kind === 'lookup' || kind === 'person' || kind === 'user') {
    const scalar = Array.isArray(value) ? value[0] : value;
    if (scalar == null || scalar === '' || Number.isNaN(Number(scalar))) return false;
  }
  const key = spColName(col);
  if (!key) return false;
  target[key] = invWriteValue(col, value, preferArray);
  return true;
}

function dualWriteInvField(target, cols, preferredNames, legacyNames, value, preferArray = false) {
  const normalized = invNormalizeWriteValue(value);
  setWritableField(target, cols, preferredNames, normalized, preferArray);
  if (Array.isArray(cols) && cols.length)
    setWritableField(target, cols, legacyNames, normalized, preferArray);
}

function invNormalizeWriteValue(value) {
  const unwrap = (v) => {
    if (v && typeof v === 'object' && !Array.isArray(v)) {
      return v.DisplayName || v.displayName || locationDisplayName(v) || v.Address?.City || v.address?.city || '';
    }
    return v;
  };
  if (Array.isArray(value)) return value.map(unwrap).filter(v => v != null && v !== '');
  return unwrap(value);
}

function stripUnwritableFields(payload, spCols) {
  const out = {};
  const derived = new Set(['ano', 'm_x00ea_s', 'n_x00ba_semana', 'mes']);
  const cols = Array.isArray(spCols) ? spCols : [];
  Object.entries(payload || {}).forEach(([key, val]) => {
    if (val == null) return;
    if (String(key).toLowerCase() === 'title') {
      out[key] = val;
      return;
    }
    if (key.endsWith('LookupId') || key.includes('@odata')) {
      out[key] = val;
      return;
    }
    const col = findSpColByNames(cols, [key]);
    if (derived.has(String(key).toLowerCase())) {
      if (col && spColIsWritable(col)) out[key] = val;
      return;
    }
    if (!cols.length || !col) {
      out[key] = val;
      return;
    }
    if (!spColIsWritable(col)) return;
    if (spColIsLocation(col) && typeof val === 'string') return;
    out[key] = val;
  });
  return out;
}

function applyInvColumnAliases(listKey, payload, spCols) {
  const lk = String(listKey || '').toLowerCase();
  const out = { ...payload };
  if (lk === 'oportunidades' || lk === 'ativos') {
    const tipos = out.TipoAtivoMatch || out.TipoAtivo || out.TipodeAtivo;
    if (tipos != null && tipos !== '') {
      delete out.TipoAtivoMatch;
      delete out.TipoAtivo;
      delete out.TipodeAtivo;
      dualWriteInvField(out, spCols, ['TipoAtivoMatch'], ['TipoAtivo', 'TipodeAtivo'], tipos, true);
    }
    const locs = out.LocalizacaoMatch || out.Localiza_x00e7__x00e3_o;
    if (locs != null && locs !== '') {
      delete out.LocalizacaoMatch;
      delete out.Localiza_x00e7__x00e3_o;
      dualWriteInvField(out, spCols, ['LocalizacaoMatch'], ['Localiza_x00e7__x00e3_o'], locs, true);
    }
  }
  if (lk === 'oportunidades') {
    const pot = out.PotencialOportunidade || out.PotencialdaOportunidade;
    if (pot != null && pot !== '') {
      delete out.PotencialOportunidade;
      delete out.PotencialdaOportunidade;
      dualWriteInvField(out, spCols, ['PotencialOportunidade'], ['PotencialdaOportunidade'], pot, true);
    }
    const areaMin = out.AreaBrutaMin ?? out.field_10;
    if (areaMin != null && areaMin !== '') {
      delete out.AreaBrutaMin;
      delete out.field_10;
      dualWriteInvField(out, spCols, ['AreaBrutaMin'], ['field_10'], areaMin);
    }
    const areaMax = out.AreaBrutaMax ?? out.AreabrutaMax_x0028_m2_x0029_;
    if (areaMax != null && areaMax !== '') {
      delete out.AreaBrutaMax;
      delete out.AreabrutaMax_x0028_m2_x0029_;
      dualWriteInvField(out, spCols, ['AreaBrutaMax'], ['AreabrutaMax_x0028_m2_x0029_'], areaMax);
    }
    const valorMin = out.ValorMinEuro ?? out.field_8;
    if (valorMin != null && valorMin !== '') {
      delete out.ValorMinEuro;
      delete out.field_8;
      dualWriteInvField(out, spCols, ['ValorMinEuro'], ['field_8'], valorMin);
    }
    const valorMax = out.ValorMaxEuro ?? out.ValorMax_x0028__x20ac__x0029_;
    if (valorMax != null && valorMax !== '') {
      delete out.ValorMaxEuro;
      delete out.ValorMax_x0028__x20ac__x0029_;
      dualWriteInvField(out, spCols, ['ValorMaxEuro'], ['ValorMax_x0028__x20ac__x0029_'], valorMax);
    }
    const rendMin = out.RendimentoMinPct ?? out.RendimentoMin_x0028__x0025__x002;
    if (rendMin != null && rendMin !== '') {
      delete out.RendimentoMinPct;
      delete out.RendimentoMin_x0028__x0025__x002;
      dualWriteInvField(out, spCols, ['RendimentoMinPct'], ['RendimentoMin_x0028__x0025__x002'], rendMin);
    }
  }
  if (lk === 'ativos') {
    const pot = out.PotencialAtivo || out.PotencialdaOportunidade;
    if (pot != null && pot !== '') {
      delete out.PotencialAtivo;
      dualWriteInvField(out, spCols, ['PotencialAtivo'], [], pot, true);
    }
    const area = out.AreaBrutaM2 ?? out.field_9;
    if (area != null && area !== '') {
      delete out.AreaBrutaM2;
      delete out.field_9;
      dualWriteInvField(out, spCols, ['AreaBrutaM2'], ['field_9'], area);
    }
    const valor = out.ValorEuro ?? out.field_8;
    if (valor != null && valor !== '') {
      delete out.ValorEuro;
      delete out.field_8;
      dualWriteInvField(out, spCols, ['ValorEuro'], ['field_8'], valor);
    }
  }
  if (lk === 'clientes') {
    const lookupCol = findSpColByNames(spCols, ['TipodeCliente']);
    const choiceCol = findSpColByNames(spCols, ['TipoCliente']);
    const lookupKind = formColumnKind(lookupCol);
    if (out.TipodeClienteLookupId != null && choiceCol && formColumnKind(choiceCol) === 'choice' && lookupKind !== 'lookup') {
      delete out.TipodeClienteLookupId;
      delete out.TipoClienteLookupId;
    }
    const tipo = out.TipoCliente || out.TipodeCliente;
    if (tipo != null && tipo !== '' && Number.isNaN(Number(tipo))) {
      delete out.TipoCliente;
      delete out.TipodeCliente;
      dualWriteInvField(out, spCols, ['TipoCliente'], ['TipodeCliente'], tipo);
    }
  }
  return stripUnwritableFields(out, spCols);
}

function formColumnChoices(colMeta) {
  const raw = colMeta?.choices || colMeta?.Choices || null;
  if (!Array.isArray(raw)) return [];
  return raw
    .map((ch) => {
      if (ch == null || ch === '') return '';
      if (typeof ch === 'string') return ch;
      return String(ch.label || ch.Label || ch.value || ch.Value || '').trim();
    })
    .filter(Boolean);
}

function isSpLocationField(key, label, value, colMeta) {
  if (isCityMultiLocationKey(key)) return false;
  const kind = formColumnKind(colMeta);
  if (kind === 'location' || kind === 'geolocation') return true;
  if (isSpLocationValue(value)) return true;
  const k = String(key || '').toLowerCase();
  const lb = foldFormLabel(label);
  if (k.includes('localiza') && k !== 'localizacaomatch') return true;
  if (lb.includes('morada') || lb.includes('endereco')) return true;
  return false;
}

function isInvMultiChoiceKey(key, label, value, colMeta) {
  if (isSpLocationField(key, label, value, colMeta)) return false;
  const k = String(key || '');
  if (INV_MULTI_CHOICE_KEYS.has(k)) return true;
  if (isInvClientesLookupKey(k)) return false;
  const kind = formColumnKind(colMeta);
  const allowMultiple = colMeta?.allowMultiple === true || colMeta?.AllowMultiple === true;
  if (allowMultiple || kind === 'multichoice' || kind === 'multichoice') return true;
  if (isPotencialFormField(k, label) || isTipoAtivoMultiField(k, label)) return true;
  const parsed = parseMaybeJson(value);
  if (Array.isArray(parsed) && parsed.length) {
    const first = parsed[0];
    if (first && typeof first === 'object' && (first.lookupId != null || first.LookupId != null)) return false;
    return parsed.every((x) => x == null || typeof x === 'string' || (x && typeof x === 'object' && (x.lookupValue || x.value || x.Title || x.DisplayName || x.displayName)));
  }
  return false;
}

function isInvClientesLookupKey(key) {
  return String(key || '').toLowerCase() === 'clientes';
}

function invMultiChoiceOptions(key, label) {
  const k = String(key || '');
  if (k === 'TipoAtivoMatch' || k === 'TipodeAtivo' || k === 'TipoAtivo' || isTipoAtivoMultiField(k, label)) return INV_TIPO_ATIVO;
  if (isPotencialFormField(k, label)) return INV_POTENCIAL;
  if (k === 'LocalizacaoMatch') return INV_LOCALIZACAO_FALLBACK;
  return [];
}

function objectDisplayText(value) {
  if (value == null || value === '') return null;
  const v = parseMaybeJson(value);
  if (v == null || v === '' || typeof v !== 'object') return null;
  if (Array.isArray(v)) {
    const parts = v.map((x) => {
      if (x == null || x === '') return '';
      if (typeof x === 'object') return x.DisplayName || x.displayName || x.lookupValue || x.value || x.Title || '';
      return String(x);
    }).map((s) => String(s).trim()).filter(Boolean);
    return parts.length ? parts.join(', ') : null;
  }
  if (isSpLocationValue(v)) {
    const name = locationDisplayName(v);
    return name || null;
  }
  const t = v.lookupValue || v.LookupValue || v.value || v.Title || v.displayName || v.DisplayName || v.email;
  if (t != null && t !== '') return String(t);
  const city = v.Address?.City || v.address?.City || v.address?.city;
  return city ? String(city) : null;
}

function invChoiceSet(value) {
  const out = [];
  const push = (v) => {
    if (v == null || v === '') return;
    if (Array.isArray(v)) { v.forEach(push); return; }
    if (typeof v === 'object') {
      const loc = v.DisplayName || v.displayName;
      if (loc) { out.push(String(loc).trim()); return; }
      const t = v.lookupValue || v.value || v.Title || v.LookupValue;
      if (t) out.push(String(t).trim());
      return;
    }
    const raw = String(v).trim();
    if (raw.startsWith('[') || raw.startsWith('{')) {
      try {
        push(JSON.parse(raw));
        return;
      } catch { /* not JSON */ }
    }
    String(v).split(/[;,|]/).forEach((p) => {
      const s = p.trim();
      if (s) out.push(s);
    });
  };
  push(value);
  return [...new Set(out)];
}

function invParseNumber(value) {
  if (value == null || value === '') return null;
  if (typeof value === 'number' && !Number.isNaN(value)) return value;
  if (typeof value === 'object') return invParseNumber(value.value ?? value.lookupValue);
  const s = String(value).replace(/\s/g, '').replace(/€/g, '').replace(/%/g, '');
  const normalized = s.includes(',') && s.includes('.')
    ? s.replace(/,/g, '')
    : s.replace(/,/g, '');
  const n = Number(normalized);
  return Number.isNaN(n) ? null : n;
}

function invLookupIds(value) {
  const ids = [];
  const push = (v) => {
    if (v == null || v === '') return;
    if (Array.isArray(v)) { v.forEach(push); return; }
    if (typeof v === 'object') {
      const id = v.lookupId ?? v.LookupId ?? v.id ?? v.Id;
      if (id != null && String(id) !== '') ids.push(String(id));
      return;
    }
    const s = String(v).trim();
    if (/^\d+$/.test(s)) ids.push(s);
  };
  push(value);
  return [...new Set(ids)];
}

function InvMultiChoiceField({ fieldKey, value, onChange, extraChoices = [], label = '' }) {
  const selected = Array.isArray(value) ? value.map(String) : invChoiceSet(value);
  const selectedSet = new Set(selected);
  const opts = [...new Set([...invMultiChoiceOptions(fieldKey, label), ...extraChoices, ...selected].filter(Boolean))];
  const toggle = (opt) => {
    const next = selectedSet.has(opt)
      ? selected.filter(s => s !== opt)
      : [...selected, opt];
    onChange(next);
  };
  return (
    <div className="inv-choice-grid" role="group" aria-label={label || undefined}>
      {opts.map(opt => (
        <button
          key={opt}
          type="button"
          className={`dash-prob-chip${selectedSet.has(opt) ? ' active' : ''}`}
          aria-pressed={selectedSet.has(opt)}
          onClick={() => toggle(opt)}
        >
          {opt}
        </button>
      ))}
    </div>
  );
}

const INV_SLIDER_AREA = { min: 0, max: 50000, step: 50 };
const INV_SLIDER_VALOR = { min: 0, max: 30000000, step: 25000 };
const INV_SLIDER_REND = { min: 0, max: 20, step: 0.1 };

function coerceRangeNumber(value) {
  if (typeof value === 'number' && Number.isFinite(value)) return value;
  if (value == null || value === '') return null;
  let text = String(value)
    .replace(/[\u20AC$%]/g, '')
    .replace(/[\u00A0\u202F]/g, '')
    .replace(/\s/g, '')
    .trim();
  if (!text) return null;
  if (/^-?\d+$/.test(text)) return Number(text);
  const commas = (text.match(/,/g) || []).length;
  const dots = (text.match(/\./g) || []).length;
  if (commas && dots) {
    const lastComma = text.lastIndexOf(',');
    const lastDot = text.lastIndexOf('.');
    text = lastComma > lastDot
      ? text.replace(/\./g, '').replace(',', '.')
      : text.replace(/,/g, '');
  } else if (commas > 1) {
    text = text.replace(/,/g, '');
  } else if (dots > 1) {
    text = text.replace(/\./g, '');
  } else if (commas === 1) {
    const frac = text.split(',')[1] || '';
    text = frac.length <= 2 ? text.replace(',', '.') : text.replace(/,/g, '');
  }
  const n = Number(text);
  return Number.isFinite(n) ? n : null;
}

function sliderNumber(value, fallback, { treatZeroAsEmpty = false } = {}) {
  const n = coerceRangeNumber(value);
  if (n == null) return fallback;
  if (treatZeroAsEmpty && n === 0) return fallback;
  return n;
}

const RANGE_MIN_RE = /(?:^|[^a-z0-9])min(?:imo)?(?:[^a-z0-9]|$)/;
const RANGE_MAX_RE = /(?:^|[^a-z0-9])max(?:imo)?(?:[^a-z0-9]|$)/;

function rangeFieldInfo(key, label) {
  const labelFold = foldFormLabel(label);
  const keyFold = foldFormLabel(String(key || '').replace(/_/g, ' '));
  const detect = `${labelFold} ${keyFold}`.replace(/\s+/g, ' ').trim();
  const isMin = RANGE_MIN_RE.test(detect);
  const isMax = RANGE_MAX_RE.test(detect);
  if (isMin === isMax) return null;
  const stemSrc = (RANGE_MIN_RE.test(labelFold) || RANGE_MAX_RE.test(labelFold)) ? labelFold : keyFold;
  const stem = stemSrc
    .replace(RANGE_MIN_RE, ' ')
    .replace(RANGE_MAX_RE, ' ')
    .replace(/[()]/g, ' ')
    .replace(/\b(m2|eur|euro|euros|pct|percentagem|percent)\b/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();
  return { role: isMin ? 'min' : 'max', stem: stem || detect };
}

function collectRangeGroups(entries) {
  const byStem = new Map();
  const groups = [];
  (entries || []).forEach(([label, key], index) => {
    const info = rangeFieldInfo(key, label);
    if (!info) return;
    let group = byStem.get(info.stem);
    if (!group) {
      group = { stem: info.stem, min: null, max: null, firstKey: key, firstIndex: index };
      byStem.set(info.stem, group);
      groups.push(group);
    }
    group[info.role] = { label, key };
  });
  return groups;
}

function rangeSliderLabel(group) {
  const src = group.min?.label || group.max?.label || group.stem || 'Intervalo';
  return String(src)
    .replace(/\s*\(?\s*(m[ií]n(?:imo)?|m[aá]x(?:imo)?)\s*\)?/gi, ' ')
    .replace(/\s+/g, ' ')
    .trim() || 'Intervalo';
}

function rangeSliderSpec(group, colMeta) {
  const sample = group.min || group.max || {};
  const format = String(
    colMeta?.format || colMeta?.Format || colMeta?.columnKind || colMeta?.ColumnKind || ''
  ).toLowerCase();
  const blob = foldFormLabel([group.stem, sample.label, sample.key, format].filter(Boolean).join(' '));
  const labelRaw = String(sample.label || '');
  const colKind = formColumnKind(colMeta);
  const moneyHint = colKind === 'currency'
    || format.includes('money')
    || format.includes('currency')
    || /\u20AC/.test(labelRaw)
    || /\b(valor|euro|money|currency|montante|ticket)\b/.test(blob);
  const percentHint = format.includes('percent')
    || /%/.test(labelRaw)
    || /\b(percent|pct|percentagem)\b/.test(blob);
  if (/\b(area|m2)\b/.test(blob) || /m\u00B2/.test(labelRaw)) {
    return {
      ...INV_SLIDER_AREA,
      kind: 'area',
      formatValue: n => `${formatNumber(n)} m\u00B2`,
    };
  }
  if (percentHint || (/\brendimento\b/.test(blob) && !moneyHint)) {
    return {
      ...INV_SLIDER_REND,
      kind: 'percent',
      formatValue: n => `${String(Math.round(n * 10) / 10).replace('.', ',')} %`,
    };
  }
  if (/\blimite\b/.test(blob)) {
    return {
      min: 0,
      max: 1000000,
      step: 1000,
      kind: 'money',
      formatValue: n => formatCurrency(n),
    };
  }
  if (moneyHint) {
    return {
      ...INV_SLIDER_VALOR,
      kind: 'money',
      formatValue: n => formatCurrency(n),
    };
  }
  return {
    min: 0,
    max: 100000,
    step: 1,
    kind: 'number',
    formatValue: n => formatNumber(n),
  };
}

function expandSliderMax(spec, ...values) {
  let max = spec.max;
  values.forEach((value) => {
    const n = coerceRangeNumber(value);
    if (n != null && n > max) {
      const step = spec.step || 1;
      max = Math.ceil(n / step) * step;
    }
  });
  return max === spec.max ? spec : { ...spec, max };
}

function rangeThumbMin(stored, specMin) {
  const n = coerceRangeNumber(stored);
  if (n == null || n < specMin) return specMin;
  return n;
}

function rangeThumbMax(stored, specMax) {
  const n = coerceRangeNumber(stored);
  if (n == null || n <= 0) return specMax;
  return n;
}

function rangePayloadValue(spec, n) {
  if (spec.kind === 'money' || spec.kind === 'percent') return String(n);
  return n;
}

function growSliderCeiling(baseMax, needed, step) {
  const unit = Number(step) > 0 ? Number(step) : 1;
  const base = Number(baseMax);
  const want = Math.max(base, Number(needed) || 0);
  if (!(want > base)) return base;
  return Math.ceil((want * 1.25) / unit) * unit;
}

function RangeSliderField({
  label,
  min = 0,
  max = 100,
  step = 1,
  minValue,
  maxValue,
  onChange,
  formatValue,
  dual = true,
}) {
  const [ceiling, setCeiling] = useState(max);
  const [draftLo, setDraftLo] = useState(null);
  const [draftHi, setDraftHi] = useState(null);

  const loRaw = sliderNumber(minValue, min);
  const hiRaw = dual ? sliderNumber(maxValue, max, { treatZeroAsEmpty: true }) : loRaw;
  const needed = Math.max(loRaw, dual ? hiRaw : loRaw);
  const trackMax = Math.max(max, ceiling, growSliderCeiling(max, needed, step));
  const lo = Math.min(trackMax, Math.max(min, Math.min(loRaw, hiRaw)));
  const hi = dual ? Math.min(trackMax, Math.max(min, Math.max(loRaw, hiRaw))) : lo;
  const span = trackMax - min || 1;
  const loPct = ((Math.min(lo, hi) - min) / span) * 100;
  const hiPct = ((Math.max(lo, hi) - min) / span) * 100;
  const fmt = typeof formatValue === 'function'
    ? formatValue
    : (n) => String(n);

  const emit = (nextLo, nextHi) => {
    if (!dual) {
      onChange(nextLo, nextLo);
      return;
    }
    const a = Math.min(nextLo, nextHi);
    const b = Math.max(nextLo, nextHi);
    onChange(a, b);
  };

  const commit = (side, text) => {
    if (side === 'lo') setDraftLo(null);
    else setDraftHi(null);
    const n = coerceRangeNumber(text);
    if (n == null) return;
    const next = Math.max(min, n);
    if (next > trackMax) setCeiling(growSliderCeiling(trackMax, next, step));
    if (!dual || side === 'lo') emit(next, dual ? Math.max(next, hi) : next);
    else emit(Math.min(lo, next), next);
  };

  const bumpMax = () => {
    const grown = growSliderCeiling(trackMax, trackMax + step, step);
    setCeiling(grown);
    emit(lo, grown);
  };

  const numInput = (side, value, draft, setDraft, aria) => (
    <input
      type="text"
      inputMode="decimal"
      className="range-slider-num"
      aria-label={aria}
      value={draft != null ? draft : fmt(value)}
      onFocus={() => setDraft(String(value))}
      onChange={e => setDraft(e.target.value)}
      onBlur={e => commit(side, e.target.value)}
      onKeyDown={e => {
        if (e.key === 'Enter') {
          e.preventDefault();
          e.currentTarget.blur();
        }
      }}
    />
  );

  return (
    <div className="range-slider">
      <div className="range-slider-head">
        <span className="range-slider-label">{label}</span>
        <div className="range-slider-value">
          {numInput('lo', lo, draftLo, setDraftLo, `${label} mínimo`)}
          {dual && <span className="range-slider-sep" aria-hidden="true">–</span>}
          {dual && numInput('hi', hi, draftHi, setDraftHi, `${label} máximo`)}
          {dual && (
            <AppTooltip label="Aumentar o máximo">
              <button
                type="button"
                className="range-slider-bump"
                aria-label={`Aumentar máximo de ${label}`}
                onClick={bumpMax}
              >
                +
              </button>
            </AppTooltip>
          )}
        </div>
      </div>
      <div className="range-slider-control">
        <div className="range-slider-track" aria-hidden="true">
          <div
            className="range-slider-fill"
            style={{ left: `${loPct}%`, width: `${Math.max(0, hiPct - loPct)}%` }}
          />
        </div>
        <input
          type="range"
          min={min}
          max={trackMax}
          step={step}
          value={lo}
          aria-label={`${label} mínimo`}
          onChange={e => {
            const next = Number(e.target.value);
            emit(next, dual ? hi : next);
          }}
        />
        {dual && (
          <input
            type="range"
            min={min}
            max={trackMax}
            step={step}
            value={hi}
            aria-label={`${label} máximo`}
            onChange={e => emit(lo, Number(e.target.value))}
          />
        )}
      </div>
    </div>
  );
}

function LocationAddressField({ value, onChange, disabled, placeholder = 'Morada, localidade ou cidade' }) {
  const text = locationDisplayName(value);
  return (
    <div className="location-address-field">
      <span className="location-address-ico" aria-hidden="true"><Icon name="location" size={14} /></span>
      <input
        type="text"
        value={text}
        disabled={disabled}
        placeholder={placeholder}
        autoComplete="street-address"
        onChange={e => onChange(buildLocationValue(e.target.value, value))}
      />
    </div>
  );
}

function findSpColumnMeta(key, label, spCols) {
  const list = spCols || [];
  const k = String(key || '').toLowerCase();
  const exact = list.find(c => String(c.name || c.Name || '').toLowerCase() === k);
  if (exact) return exact;
  if (isTipoClienteFormField(key, label)) {
    return list.find(c => {
      const n = String(c.name || c.Name || '').toLowerCase();
      const d = foldFormLabel(c.displayName || c.DisplayName || '');
      return n.includes('tipodecliente') || n.includes('tipocliente')
        || (d.includes('tipo') && d.includes('cliente') && !d.includes('ativo'));
    }) || null;
  }
  const wanted = foldFormLabel(label);
  if (wanted) {
    const byDisplay = list.find(c => foldFormLabel(c.displayName || c.DisplayName) === wanted);
    if (byDisplay) return byDisplay;
  }
  return null;
}

function resolveFormFieldMeta(key, columnDefs, spCols, label) {
  const k = String(key || '').toLowerCase();
  const def = (columnDefs || []).find(c => String(c.key || '').toLowerCase() === k);
  const sp = findSpColumnMeta(key, label, spCols);
  return {
    columnKind: formColumnKind(def) || formColumnKind(sp),
    format: def?.format || def?.Format || null,
    allowMultiple: !!(def?.allowMultiple || def?.AllowMultiple || sp?.allowMultiple || sp?.AllowMultiple),
    choices: [...new Set([...formColumnChoices(def), ...formColumnChoices(sp)])],
    lookupListId: sp?.lookupListId || sp?.LookupListId || def?.lookupListId || def?.LookupListId || null,
    lookupListName: sp?.lookupListName || sp?.LookupListName || def?.lookupListName || def?.LookupListName || null,
    spName: sp?.name || sp?.Name || key,
    readOnly: !!(sp?.readOnly || sp?.ReadOnly || def?.readOnly || def?.ReadOnly),
    isSystem: !!(sp?.isSystem || sp?.IsSystem),
    calculatedFormula: sp?.calculatedFormula || sp?.CalculatedFormula || null,
  };
}

function InvClientesPicker({ value, onChange }) {
  const selected = Array.isArray(value) ? value.map(String) : invLookupIds(value);
  const selectedSet = new Set(selected);
  const [clients, setClients] = useState([]);
  const [query, setQuery] = useState('');
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      setLoading(true);
      try {
        const items = await fetchListItemsPages('clientes', { pageSize: 100, maxPages: 8 });
        if (!cancelled) setClients(items);
      } catch {
        if (!cancelled) setClients([]);
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, []);

  const q = query.trim().toLowerCase();
  const visible = clients.filter((c) => {
    const title = String(displayMatchValue(c, 'Title', 'title') || '').toLowerCase();
    const email = String(displayMatchValue(c, 'Email', 'email') || '').toLowerCase();
    if (!q) return true;
    return title.includes(q) || email.includes(q);
  });

  const toggle = (id) => {
    const next = selectedSet.has(id)
      ? selected.filter(s => s !== id)
      : [...selected, id];
    onChange(next);
  };

  return (
    <div className="inv-clientes-picker">
      <input
        type="search"
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Pesquisar cliente"
        aria-label="Pesquisar cliente"
      />
      {loading && <p className="form-hint">A carregar clientes…</p>}
      {!loading && visible.length === 0 && (
        <p className="form-hint">{q ? 'Nenhum cliente encontrado.' : 'Sem clientes.'}</p>
      )}
      <div className="inv-choice-grid inv-choice-grid-scroll" role="group" aria-label="Clientes">
        {visible.map((c) => {
          const id = String(c.id || c.Id || '');
          const title = displayMatchValue(c, 'Title', 'title') || `Cliente ${id}`;
          const on = selectedSet.has(id);
          return (
            <button
              key={id}
              type="button"
              className={`dash-prob-chip${on ? ' active' : ''}`}
              aria-pressed={on}
              onClick={() => toggle(id)}
            >
              {title}
            </button>
          );
        })}
      </div>
    </div>
  );
}

function parseAppRoute() {
  let raw = window.location.pathname.replace(/^\//, '').trim();
  if (raw === 'index.html') raw = '';

  if (!raw && window.location.hash.startsWith('#/')) {
    raw = window.location.hash.replace(/^#\/?/, '').trim();
  }

  const parts = raw.split('/').filter(Boolean);
  if (parts.length === 0) {
    return { page: 'dashboard', tab: null, listKey: null };
  }
  if (parts[0] === 'settings') {
    if (parts[1] === 'columns' && parts[2]) {
      return { page: 'settings', tab: 'columns', listKey: parts[2] };
    }
    return { page: 'settings', tab: null, listKey: null };
  }
  if (parts[0] === 'theme') {
    return { page: 'theme', tab: null, listKey: null };
  }
  if (parts[0] === 'perfil') {
    return {
      page: 'perfil',
      tab: parts[1] ? decodeURIComponent(parts[1]) : null,
      listKey: null,
    };
  }
  const page = PAGE_IDS.includes(parts[0]) ? parts[0] : 'dashboard';
  return { page, tab: parts[1] || null, listKey: null };
}

function profileSlugFromEmail(email) {
  if (!email) return null;
  const local = String(email).trim().split('@')[0].trim().toLowerCase();
  return local || null;
}

function buildAppRoute(page, tab, listKey) {
  if (page === 'settings') {
    if (tab === 'columns' && listKey) return `/settings/columns/${encodeURIComponent(listKey)}`;
    return '/settings';
  }
  if (page === 'theme') return '/theme';
  if (page === 'perfil') {
    if (tab) {
      const slug = String(tab).includes('@') ? profileSlugFromEmail(tab) : String(tab).trim().toLowerCase();
      return slug ? `/perfil/${encodeURIComponent(slug)}` : '/perfil';
    }
    return '/perfil';
  }
  const defaultTab = ROUTE_DEFAULT_TABS[page];
  if (!tab || tab === defaultTab) {
    return `/${page}`;
  }
  return `/${page}/${tab}`;
}

/** Canonical page-filter query keys (toolbar / Explorar deep-links). */
const PAGE_FILTER_KEYS = Object.freeze([
  'q', 'ano', 'mes', 'estado', 'liquidado', 'operacao', 'natureza', 'probabilidade', 'novo',
]);

function parsePageFiltersFromSearch(search = window.location.search) {
  const raw = String(search || '');
  const params = new URLSearchParams(raw.startsWith('?') || raw === '' ? raw : `?${raw}`);
  const out = {};
  for (const key of PAGE_FILTER_KEYS) {
    if (!params.has(key)) continue;
    out[key] = String(params.get(key) ?? '').trim();
  }
  return out;
}

function buildPageSearch(filters, allowedKeys = PAGE_FILTER_KEYS) {
  const params = new URLSearchParams();
  const keys = Array.isArray(allowedKeys) && allowedKeys.length ? allowedKeys : PAGE_FILTER_KEYS;
  for (const key of keys) {
    const val = filters?.[key];
    if (val == null) continue;
    const text = String(val).trim();
    if (!text) continue;
    params.set(key, text);
  }
  const qs = params.toString();
  return qs ? `?${qs}` : '';
}

function buildPageUrl(page, tab, filters, listKey = null, allowedKeys = PAGE_FILTER_KEYS) {
  return `${buildAppRoute(page, tab, listKey)}${buildPageSearch(filters, allowedKeys)}`;
}

/** Replace managed filter keys in the URL; preserve unrelated query params. */
function replaceManagedPageFiltersInUrl(filters, managedKeys = PAGE_FILTER_KEYS) {
  const params = new URLSearchParams(window.location.search);
  const keys = Array.isArray(managedKeys) && managedKeys.length ? managedKeys : PAGE_FILTER_KEYS;
  for (const key of keys) params.delete(key);
  for (const key of keys) {
    const val = filters?.[key];
    if (val == null) continue;
    const text = String(val).trim();
    if (!text) continue;
    params.set(key, text);
  }
  const qs = params.toString();
  const next = `${window.location.pathname}${qs ? `?${qs}` : ''}`;
  const current = `${window.location.pathname}${window.location.search}`;
  if (next !== current) {
    window.history.replaceState(null, '', next);
  }
}

function pageUrlHasFilterKeys(search = window.location.search) {
  return Object.keys(parsePageFiltersFromSearch(search)).length > 0;
}

function normalizeRouteTab(page, tab) {
  if (!tab) return ROUTE_DEFAULT_TABS[page] || null;

  // Legacy Cards/Lista routes → Equipa directory (list mode is a toolbar toggle now).
  if (page === 'equipa') {
    if (['cards', 'lista'].includes(tab)) return 'main';
    if (['main', 'recrutamento', 'comunicacaointerna'].includes(tab)) return tab;
  }
  if (page === 'mediacao' && ['negocios', 'faturacao', 'comissao'].includes(tab)) return tab;
  if (page === 'carteira') {
    if (CARTEIRA_SLUG_TO_ID[tab]) return tab;
    if (CARTEIRA_ID_TO_SLUG[tab]) return CARTEIRA_ID_TO_SLUG[tab];
  }

  return ROUTE_DEFAULT_TABS[page] || null;
}

/** Investimentos lists: consultors may create/update (API mirrors this). */
function isInvestimentosListKey(listKey) {
  const k = String(listKey || '').toLowerCase();
  return k === 'ativos' || k === 'oportunidades' || k === 'clientes';
}

function userCanWriteInvestimentos(user, listKey) {
  return isInvestimentosListKey(listKey) && (userIsConsultor(user) || userIsAdmin(user));
}

function userCanAccessRecrutamentoCmi(u) {
  // Power Apps: "C" or "A" in HABITA Permissões.
  return Boolean(userIsAdmin(u) || u?.canCreate || u?.CanCreate);
}

const EQUIPA_LIST_VIEW_KEY = 'myhabita.equipaListView';

function loadEquipaListView() {
  try {
    const v = window.localStorage.getItem(EQUIPA_LIST_VIEW_KEY);
    return v === 'lista' || v === 'table' ? 'lista' : 'cards';
  } catch {
    return 'cards';
  }
}

function saveEquipaListView(view) {
  try {
    window.localStorage.setItem(EQUIPA_LIST_VIEW_KEY, view === 'lista' ? 'lista' : 'cards');
  } catch { /* ignore */ }
}

function useEquipaListView() {
  const [view, setViewState] = useState(loadEquipaListView);
  const setView = useCallback((next) => {
    const v = next === 'lista' || next === 'table' ? 'lista' : 'cards';
    setViewState(v);
    saveEquipaListView(v);
  }, []);
  return [view, setView];
}

function EquipaListViewToggle({ view, onChange }) {
  return (
    <div className="phone-list-view-toggle equipa-list-view-toggle" role="group" aria-label="Vista Cards ou Lista">
      <button
        type="button"
        className={`toolbar-icon-btn phone-view-btn${view === 'cards' ? ' active' : ''}`}
        title="Cards"
        aria-pressed={view === 'cards'}
        onClick={() => onChange('cards')}
      >
        <Icon name="layout" size={15} />
      </button>
      <button
        type="button"
        className={`toolbar-icon-btn phone-view-btn${view === 'lista' ? ' active' : ''}`}
        title="Lista"
        aria-pressed={view === 'lista'}
        onClick={() => onChange('lista')}
      >
        <Icon name="kanban" size={15} />
      </button>
    </div>
  );
}

function carteiraTabFromRoute(tab) {
  const slug = normalizeRouteTab('carteira', tab);
  if (slug === 'matching') return 'matching';
  return CARTEIRA_SLUG_TO_ID[slug] || CARTEIRA_SLUG_TO_ID.carteira;
}

function MicrosoftIcon() {
  return (
    <svg viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
      <rect x="1" y="1" width="9" height="9" fill="#f25022"/>
      <rect x="11" y="1" width="9" height="9" fill="#7fba00"/>
      <rect x="1" y="11" width="9" height="9" fill="#00a4ef"/>
      <rect x="11" y="11" width="9" height="9" fill="#ffb900"/>
    </svg>
  );
}

function LoginPage({ loginUrl, checking = false }) {
  const [starting, setStarting] = useState(false);
  const authError = new URLSearchParams(window.location.search).get('authError');
  const href = loginUrl || '/login';
  const busy = checking || starting;

  return (
    <div className="login-screen">
      <div className="login-card">
        <BrandLogo variant="myhabita" className="login-logo-img" />
        <p className="login-sub">Gestão Imobiliária</p>
        {authError === 'invalid_client_secret' && (
          <p className="login-error">
            Client secret inválido. No Entra, copie o <strong>Value</strong> do secret (não o Secret ID) para{' '}
            <code>Data/app-settings.local.json</code> e reinicie a app.
          </p>
        )}
        {authError === 'signin_failed' && (
          <p className="login-error">Falha ao iniciar sessão. Tente novamente ou contacte o administrador.</p>
        )}
        {busy ? (
          <p className="login-status" role="status" aria-live="polite">
            {starting ? 'A iniciar sessão…' : 'A verificar sessão…'}
          </p>
        ) : (
          <a
            className="btn-microsoft"
            href={href}
            onClick={() => setStarting(true)}
          >
            <MicrosoftIcon />
            Iniciar sessão com Microsoft
          </a>
        )}
        <p className="login-hint">
          Se ficar preso no ecrã de login,{' '}
          <a href="/logout">limpe a sessão</a> e tente de novo.
        </p>
      </div>
      <div className="login-brands">
        <BrandLogo variant="himo" className="login-brand-logo login-brand-logo-himo" />
        <BrandLogo variant="habitaWhite" className="login-brand-logo login-brand-logo-habita" />
      </div>
    </div>
  );
}

function userAssinatura(name) {
  if (!name) return 'Utilizador';
  const short = String(name).split('|')[0].trim();
  return short || 'Utilizador';
}

/** Fun sidebar avatar masks — only Livia Moreira (random on load). */
const LIVIA_AVATAR_MASKS = [
  '/images/avatar-mask-livia-clown.png',
  '/images/avatar-mask-livia-rainbow.png',
  '/images/avatar-mask-livia-angel.png',
  '/images/avatar-mask-livia-popcat.png',
];

function pickLiviaAvatarMask() {
  return LIVIA_AVATAR_MASKS[Math.floor(Math.random() * LIVIA_AVATAR_MASKS.length)];
}

function isLiviaMoreira(user) {
  if (!user) return false;
  const normalize = (s) => String(s || '')
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .toLowerCase()
    .trim();
  const name = normalize(userAssinatura(user.displayName || user.DisplayName || user.assinatura || user.Assinatura));
  const email = normalize(user.email || user.Email);
  if (email.includes('livia.moreira') || /^livia[._-]?moreira@/.test(email)) return true;
  return /\blivia\s+moreira\b/.test(name);
}

function userSigla(name) {
  if (!name) return '?';
  const parts = String(name).trim().split(/\s+/).filter(Boolean);
  if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
  return String(name).slice(0, 2).toUpperCase();
}

function userIsGestorOperacional(u) {
  const f = foldFormLabel(u?.funcao || u?.Funcao || '');
  if (/gestor(?:a)?(?:\s+de)?\s+operacional\b/.test(f)) return true;
  const roles = u?.roles || u?.Roles || [];
  return roles.some(r => /gestor(?:a)?(?:\s+de)?\s+operacional\b/.test(foldFormLabel(r)));
}

function userIsAdmin(u) {
  if (!u) return false;
  // HABITA - Permissões code "A" maps to isAdmin: full CRUD + all operações/localizações.
  if (u.isAdmin || u.IsAdmin) return true;
  // Gestor Operacional has the same app powers as Administrador.
  if (userIsGestorOperacional(u)) return true;
  const roles = u.roles || u.Roles || [];
  // Some SharePoint/Entra payloads encode admin as a single-letter code.
  if (roles.some(r => String(r || '').trim().toUpperCase() === 'A')) return true;
  return roles.some(r => /admin/i.test(String(r)));
}

function userCanViewAs(u) {
  // HABITA - Permissões: code "A" => Admin (all). Only those users can "Entrar como".
  return Boolean(userIsAdmin(u));
}

/** Settings "Perfis": permission roles, else operação roles, else função label. */
function formatUserPerfisLabel(user, isAdminFlag = false) {
  const roles = (user?.roles || user?.Roles || [])
    .map(r => String(r || '').trim())
    .filter(Boolean);
  if (roles.length) return roles.join(', ');
  if (isAdminFlag || userIsAdmin(user)) return 'Administrador';
  const ops = (user?.allowedOperacoes || user?.AllowedOperacoes || [])
    .map(o => String(o || '').trim())
    .filter(Boolean);
  if (ops.length) return `Operação: ${ops.join(', ')}`;
  const funcao = user?.funcao || user?.Funcao;
  if (funcao) return String(funcao);
  return 'Sem perfil de permissões';
}

function commissionMetrics(user) {
  const sum = Number(
    user?.sumComissaoAprovada ??
    user?.SumComissaoAprovada ??
    user?.sumComissao ??
    user?.SumComissao ??
    0
  );
  // Current volume faixa from Plano Comissional (limite_min / limite_max), amount-driven.
  const bandMin = Number(user?.limiteMin ?? user?.LimiteMin ?? 0);
  const rawMax = user?.limiteMax ?? user?.LimiteMax;
  const bandMax = rawMax == null || rawMax === '' ? NaN : Number(rawMax);
  const objectivo = Number(user?.objectivo ?? user?.Objectivo ?? 0);
  const apiPct = Number(user?.progressPct ?? user?.ProgressPct);

  let segmentPct = 0;
  if (Number.isFinite(bandMax) && bandMax > bandMin) {
    // Primary: how far through the Plano faixa (min → max).
    segmentPct = Math.min(Math.max(((sum - bandMin) / (bandMax - bandMin)) * 100, 0), 100);
  } else if (Number.isFinite(apiPct)) {
    segmentPct = Math.min(Math.max(apiPct, 0), 100);
  } else if (objectivo > bandMin) {
    segmentPct = Math.min(Math.max(((sum - bandMin) / (objectivo - bandMin)) * 100, 0), 100);
  } else if (objectivo > 0) {
    segmentPct = Math.min((sum / objectivo) * 100, 100);
  }

  const objectivoPct = objectivo > 0 ? Math.min((sum / objectivo) * 100, 100) : segmentPct;
  const comissaoPct = segmentPct;
  // Escalão display = amount-based faixa index; ring segments = all faixas in the plan.
  const faixaIndex = Number(user?.faixaIndex ?? user?.FaixaIndex ?? user?.escalao ?? user?.Escalao);
  const faixaCount = Number(user?.faixaCount ?? user?.FaixaCount);
  const segmentCount = Number.isFinite(faixaCount) && faixaCount > 0 ? faixaCount : 4;
  const faixaPos = Number.isFinite(faixaIndex) && faixaIndex > 0 ? faixaIndex : 1;
  const filledAmount = (faixaPos - 1) + (segmentPct / 100);

  return {
    sum,
    min: bandMin,
    max: Number.isFinite(bandMax) ? bandMax : 0,
    hasOpenMax: false,
    objectivo,
    objectivoPct,
    comissaoPct,
    segmentPct,
    filledAmount,
    segmentCount,
    faixaIndex: faixaPos,
    faixaCount: segmentCount,
    planoEscalao: user?.planoEscalao ?? user?.PlanoEscalao ?? null,
  };
}

function userHasCommissionProgress(user) {
  if (!user) return false;
  // Admins / non-consultors never show the escalão meter.
  if (userIsAdmin(user)) return false;
  if (userIsDiretorGeral(user)) return !!(user.existeComissao ?? user.ExisteComissao);
  if (!(user.existeComissao ?? user.ExisteComissao)) return false;
  return true;
}

const CANAL_HABITA_LABEL = 'Canal HABITA';

function userIsDiretorGeral(user) {
  return equipaIsDiretorGeral(user?.funcao || user?.Funcao);
}

/** Person label on commission widgets — Diretor Geral is Canal HABITA, not a consultor. */
function commissionActorLabel(user) {
  if (userIsDiretorGeral(user)) return CANAL_HABITA_LABEL;
  const raw = user?.displayName || user?.DisplayName || user?.assinatura || user?.Assinatura || '';
  const first = String(raw).split('|')[0].trim().split(/\s+/).filter(Boolean)[0];
  return first || 'Eu';
}

/** Front-line consultor (not admin / gestor / diretor). Used for personal dashboard widgets. */
function userIsConsultor(user) {
  if (!user || userIsAdmin(user) || userIsDiretorGeral(user)) return false;
  const roles = (user.roles || user.Roles || []).map(r => String(r));
  if (roles.some(r => /consultor/i.test(r))) return true;
  if (roles.some(r => /gestor|diretor|daf|administrad/i.test(r))) return false;
  // Escalão Comissional row → commission meter → treat as consultor for widgets.
  if (user.existeComissao ?? user.ExisteComissao) return true;
  const metrics = commissionMetrics(user);
  return metrics.min > 0 || metrics.sum > 0;
}

/** Admins/gestores may reassign Consultor; consultors are locked to themselves. */
function userCanChangeConsultor(user) {
  if (userIsAdmin(user)) return true;
  if (userIsConsultor(user)) return false;
  return true;
}

function commissionPercents(user) {
  const { objectivoPct, comissaoPct } = commissionMetrics(user);
  return { objectivoPct, comissaoPct };
}

function describeCommissionArc(cx, cy, r, startAngle, sweepAngle) {
  if (sweepAngle <= 0.001) return '';
  const x1 = cx + r * Math.cos(startAngle);
  const y1 = cy + r * Math.sin(startAngle);
  const x2 = cx + r * Math.cos(startAngle + sweepAngle);
  const y2 = cy + r * Math.sin(startAngle + sweepAngle);
  const large = sweepAngle > Math.PI ? 1 : 0;
  return `M ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2}`;
}

function CommissionRingSegments({ filledAmount, escalao, segmentCount = 4, size = 56, animate = true }) {
  const cx = 50;
  const cy = 50;
  const r = 40;
  // Power Apps horseshoe: gap at the bottom, arc runs lower-left → top → lower-right.
  // Parametric match: start at 3π/4 in standard SVG angles (y-down).
  const arcStart = (3 * Math.PI) / 4;
  const maxSweep = 1.5 * Math.PI;
  const gapRad = 0.07;
  const segSweep = (maxSweep - gapRad * segmentCount) / segmentCount;
  const trackColor = '#e6e6e6';
  // Power Apps uses SecondaryColor (Habita green) for commission progress fill.
  const fillColor = window.MyHabitaTheme?.colors?.primary2 || '#2D9C48';

  const target = Math.max(0, Math.min(Number(filledAmount) || 0, segmentCount));
  const [displayFill, setDisplayFill] = useState(() => (animate ? 0 : target));
  const animRef = useRef(null);

  useEffect(() => {
    if (!animate) {
      setDisplayFill(target);
      return undefined;
    }

    const reduceMotion = typeof window !== 'undefined'
      && window.matchMedia
      && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reduceMotion) {
      setDisplayFill(target);
      return undefined;
    }

    const from = 0;
    const to = target;
    const duration = 1100;
    const start = performance.now();
    if (animRef.current) cancelAnimationFrame(animRef.current);
    setDisplayFill(0);

    const easeOutCubic = (t) => 1 - ((1 - t) ** 3);
    const tick = (now) => {
      const t = Math.min(1, (now - start) / duration);
      setDisplayFill(from + (to - from) * easeOutCubic(t));
      if (t < 1) animRef.current = requestAnimationFrame(tick);
      else animRef.current = null;
    };
    animRef.current = requestAnimationFrame(tick);
    return () => {
      if (animRef.current) cancelAnimationFrame(animRef.current);
    };
  }, [target, animate, segmentCount]);

  const segments = [];
  for (let i = 0; i < segmentCount; i += 1) {
    const segStart = arcStart + i * (segSweep + gapRad);
    let fillPortion = 0;
    if (displayFill >= i + 1) fillPortion = 1;
    else if (displayFill > i) fillPortion = displayFill - i;

    segments.push(
      <g key={i}>
        <path
          d={describeCommissionArc(cx, cy, r, segStart, segSweep)}
          fill="none"
          stroke={trackColor}
          strokeWidth="10"
          strokeLinecap="butt"
        />
        {fillPortion > 0.01 && (
          <path
            d={describeCommissionArc(cx, cy, r, segStart, segSweep * fillPortion)}
            fill="none"
            stroke={fillColor}
            strokeWidth="10"
            strokeLinecap="butt"
          />
        )}
      </g>
    );
  }

  return (
    <svg viewBox="0 0 100 100" className="commission-ring-svg" aria-hidden="true">
      {segments}
    </svg>
  );
}

function CommissionRingDual({ objectivoPct, comissaoPct, escalao, size = 56 }) {
  const cx = 50;
  const cy = 50;
  const r = 40;
  const start = Math.PI / 4;
  const maxSweep = 1.5 * Math.PI;

  const describeArc = (sweep) => {
    if (sweep <= 0) return '';
    const x1 = cx + r * Math.cos(start);
    const y1 = cy + r * Math.sin(start);
    const x2 = cx + r * Math.cos(start + sweep);
    const y2 = cy + r * Math.sin(start + sweep);
    const large = sweep > Math.PI ? 1 : 0;
    return `M ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2}`;
  };

  const objSweep = (Math.min(objectivoPct, 100) / 100) * maxSweep;
  const comSweep = (Math.min(comissaoPct, 100) / 100) * maxSweep;
  const track = '#e6e6e6';
  const objectivoColor = escalaoBarColor(escalao, 'mid');
  const comissaoColor = window.MyHabitaTheme?.colors?.primary2 || '#2D9C48';

  return (
    <svg viewBox="0 0 100 100" width={size} height={size} className="commission-ring-svg" aria-hidden="true">
      <path d={describeArc(maxSweep)} fill="none" stroke={track} strokeWidth="14" strokeLinecap="butt" />
      {objSweep > 0.01 && (
        <path d={describeArc(objSweep)} fill="none" stroke={objectivoColor} strokeWidth="14" strokeLinecap="butt" strokeDasharray="45 3" />
      )}
      {comSweep > 0.01 && (
        <path d={describeArc(comSweep)} fill="none" stroke={comissaoColor} strokeWidth="14" strokeLinecap="butt" strokeDasharray="45 3" />
      )}
    </svg>
  );
}

function AvatarCommissionGauge({ pct, size = 56 }) {
  const clamped = Math.max(0, Math.min(100, Number(pct) || 0));
  const cx = 50;
  const cy = 50;
  const r = 44;
  const circumference = 2 * Math.PI * r;
  const dash = (clamped / 100) * circumference;
  const color = window.MyHabitaTheme?.colors?.primary2 || '#2D9C48';

  return (
    <svg viewBox="0 0 100 100" width={size} height={size} className="commission-ring-svg" aria-hidden="true">
      <circle cx={cx} cy={cy} r={r} fill="none" stroke="#e6ebf3" strokeWidth="9" />
      <circle
        cx={cx}
        cy={cy}
        r={r}
        fill="none"
        stroke={color}
        strokeWidth="9"
        strokeLinecap="round"
        strokeDasharray={`${dash} ${circumference - dash}`}
        transform="rotate(-90 50 50)"
      />
    </svg>
  );
}

function EscalaoBadge({ escalao, variant = 'inline' }) {
  if (escalao == null) return null;
  if (variant === 'avatar') {
    return (
      <span className="user-escalao-chip" aria-hidden="true">
        {escalao}
      </span>
    );
  }
  const c1 = escalaoBarColor(escalao, 'light');
  const c2 = escalaoBarColor(escalao, 'mid');
  const c3 = escalaoBarColor(escalao, 'dark');
  return (
    <div className="escalao-badge" title={`Escalão ${escalao}`}>
      <svg viewBox="0 0 40 24" width="40" height="16" aria-hidden="true" className="escalao-bars">
        <rect x="2" y="10" width="8" height="12" rx="1" fill={c1} />
        <rect x="14" y="4" width="8" height="18" rx="1" fill={c2} />
        <rect x="26" y="12" width="8" height="10" rx="1" fill={c3} />
      </svg>
      <span className="escalao-badge-num">{escalao}</span>
    </div>
  );
}

function escalaoBarColor(escalao, variant = 'mid') {
  if (escalao === 1) return variant === 'light' ? '#8c8bab' : variant === 'dark' ? '#4a4970' : '#5c5b85';
  if (escalao === 2) return variant === 'light' ? '#6a698f' : variant === 'dark' ? '#3a3958' : '#4a4970';
  if (escalao === 3) return variant === 'light' ? '#b8b7d0' : variant === 'dark' ? '#6a698f' : '#8c8bab';
  return window.MyHabitaTheme?.colors?.primary1 || '#191847';
}

function UserAvatarInner({ photoUrl, displayName, collapsed, showRing }) {
  const [failed, setFailed] = useState(false);

  useEffect(() => {
    setFailed(false);
  }, [photoUrl]);

  if (photoUrl && !failed) {
    return (
      <img
        src={photoUrl}
        alt=""
        className="user-avatar-photo"
        referrerPolicy="no-referrer"
        onError={() => setFailed(true)}
      />
    );
  }

  if (collapsed) {
    return <span className="user-sigla">{userSigla(displayName)}</span>;
  }

  return <Icon name="user" size={showRing ? 22 : 24} />;
}

function userProfilePhotoUrl(user, { isViewingAs = false } = {}) {
  if (!user) return null;
  const email = user.email || user.Email;
  const colaboradorId = user.colaboradorId || user.ColaboradorId || user.id || user.Id || null;
  if (colaboradorId) {
    return `/api/lists/himo-colaboradores/items/${encodeURIComponent(String(colaboradorId))}/image/Imagem?size=smallSquare`;
  }
  if (!email) return null;
  const params = new URLSearchParams({ email: String(email) });
  if (isViewingAs) params.set('viewAsEmail', String(email));
  return `/api/equipa/photo?${params.toString()}`;
}

function SidebarUserProfile({ user, collapsed, isLiveConnected, isViewingAs, onExitViewAs, onOpenProfile }) {
  const metrics = commissionMetrics(user);
  // Only consultors with Escalão Comissional context get the meter — never admins.
  const showRing = !userIsAdmin(user) && userHasCommissionProgress(user);
  const displayName = userAssinatura(user?.displayName);
  const roleLabel = user?.funcao || user?.Funcao || user?.roles?.[0] || 'Consultor';
  const email = user?.email || user?.Email;
  // Prefer cached Colaboradores Imagem (server disk); include id so /photo can resolve without Graph.
  const photoUrl = userProfilePhotoUrl(user, { isViewingAs });
  const showLiviaMask = isLiviaMoreira(user);
  const [liviaMaskSrc, setLiviaMaskSrc] = useState(pickLiviaAvatarMask);
  const [tipOpen, setTipOpen] = useState(false);
  const [tipPos, setTipPos] = useState(null);
  const tipAnchorRef = useRef(null);
  useEffect(() => {
    if (showLiviaMask) setLiviaMaskSrc(pickLiviaAvatarMask());
  }, [showLiviaMask, email]);
  const frameSize = collapsed ? (showLiviaMask ? 56 : 44) : (showLiviaMask ? 96 : 80);

  // Sidebar: current faixa only (not overall plan progress — that stays on Perfil).
  const filledCurrentFaixa = Math.min(1, Math.max(0, metrics.segmentPct / 100));
  const escalao = user?.escalao ?? user?.Escalao ?? null;
  const pctLabel = `${Math.round(metrics.segmentPct)}%`;

  const updateTipPos = useCallback(() => {
    const el = tipAnchorRef.current;
    if (!el) return;
    const rect = el.getBoundingClientRect();
    if (collapsed) {
      setTipPos({
        top: rect.bottom + 8,
        left: rect.left + rect.width / 2,
        place: 'below',
      });
    } else {
      setTipPos({
        top: rect.top + rect.height / 2,
        left: rect.right + 10,
        place: 'right',
      });
    }
  }, [collapsed]);

  useEffect(() => {
    if (!tipOpen || !showRing) {
      setTipPos(null);
      return undefined;
    }
    updateTipPos();
    window.addEventListener('scroll', updateTipPos, true);
    window.addEventListener('resize', updateTipPos);
    return () => {
      window.removeEventListener('scroll', updateTipPos, true);
      window.removeEventListener('resize', updateTipPos);
    };
  }, [tipOpen, showRing, updateTipPos]);

  const openProfile = () => {
    if (typeof onOpenProfile === 'function') onOpenProfile(user);
  };

  const tipNode = showRing && tipOpen && tipPos
    ? ReactDOM.createPortal(
        <div
          className={`user-commission-tip toast toast-success is-portal place-${tipPos.place}`}
          role="tooltip"
          style={{ top: tipPos.top, left: tipPos.left }}
        >
          <span className="toast-icon" aria-hidden="true">
            <Icon name="graph" size={14} />
          </span>
          <div className="toast-body">
            <div className="toast-kicker">
              {escalao != null ? `Escalão ${escalao}` : 'Comissão'}
              {user?.anoComissao || user?.AnoComissao
                ? ` · ${user.anoComissao || user.AnoComissao}`
                : ''}
            </div>
            <div className="toast-message">
              {formatCurrency(metrics.sum)}
              {metrics.max > metrics.min
                ? ` · faixa ${formatCurrency(metrics.min)} – ${formatCurrency(metrics.max)}`
                : metrics.objectivo > 0
                  ? ` · objectivo ${formatCurrency(metrics.objectivo)}`
                  : ''}
            </div>
            <div className="toast-title">{pctLabel} na faixa</div>
          </div>
        </div>,
        document.body
      )
    : null;

  return (
    <div className={`user-block ${collapsed ? 'user-block-collapsed' : ''} ${showLiviaMask ? 'user-block-livia' : ''}`}>
      <div className="user-avatar-stack">
        <div
          ref={tipAnchorRef}
          className="user-avatar-tip-wrap"
          onMouseEnter={() => showRing && setTipOpen(true)}
          onMouseLeave={() => setTipOpen(false)}
          onFocus={() => showRing && setTipOpen(true)}
          onBlur={() => setTipOpen(false)}
        >
          <button
            type="button"
            className={`user-avatar-frame ${showRing ? 'has-ring' : ''} ${showLiviaMask ? 'has-fun-mask' : ''} is-clickable`}
            aria-label={
              escalao != null
                ? `Ver perfil de ${displayName}, Escalão ${escalao}`
                : `Ver perfil de ${displayName}`
            }
            style={{ width: frameSize, height: frameSize }}
            onClick={openProfile}
          >
            {showRing && (
              <CommissionRingSegments
                filledAmount={filledCurrentFaixa}
                escalao={escalao}
                segmentCount={1}
              />
            )}
            <div className={`user-avatar ${showRing ? 'user-avatar-inset' : ''}`}>
              <UserAvatarInner
                photoUrl={photoUrl}
                displayName={displayName}
                collapsed={collapsed}
                showRing={showRing}
              />
            </div>
            {showLiviaMask && (
              <img
                src={liviaMaskSrc}
                alt=""
                className="user-avatar-fun-mask"
                draggable={false}
              />
            )}
            {showRing && escalao != null && <EscalaoBadge variant="avatar" escalao={escalao} />}
          </button>
        </div>
        {tipNode}
      </div>

      {!collapsed && (
        <>
          <button type="button" className="user-name user-name-btn" onClick={openProfile} title="Ver perfil">
            {displayName}
          </button>
          <div className="user-role-line">{roleLabel}</div>
          {email && <div className="user-email-line">{email}</div>}
          {isViewingAs && (
            <button type="button" className="view-as-exit-btn" onClick={onExitViewAs}>
              Voltar ao meu perfil
            </button>
          )}
        </>
      )}
    </div>
  );
}

function HeaderConsultorMeter({ user }) {
  const { isPhone } = useViewportNav();
  if (!isPhone || !user) return null;

  const metrics = commissionMetrics(user);
  const showRing = !userIsAdmin(user) && userHasCommissionProgress(user);
  const displayName = userAssinatura(user?.displayName);
  const email = user?.email || user?.Email;
  const photoUrl = userProfilePhotoUrl(user);
  const escalao = user?.escalao ?? user?.Escalao ?? null;
  const filledCurrentFaixa = Math.min(1, Math.max(0, metrics.segmentPct / 100));
  const pctLabel = `${Math.round(metrics.segmentPct)}%`;

  const openProfile = () => {
    const slug = profileSlugFromEmail(email);
    const path = slug ? `/perfil/${encodeURIComponent(slug)}` : '/perfil';
    if (window.location.pathname === path) return;
    window.history.pushState(null, '', path);
    window.dispatchEvent(new PopStateEvent('popstate'));
  };

  return (
    <div className="header-consultor-meter">
      <button
        type="button"
        className={`user-avatar-frame header-consultor-frame ${showRing ? 'has-ring' : ''} is-clickable`}
        aria-label={
          showRing
            ? (escalao != null
              ? `Perfil de ${displayName}, Escalão ${escalao}, ${pctLabel} na faixa`
              : `Perfil de ${displayName}, ${pctLabel} na faixa`)
            : `Perfil de ${displayName}`
        }
        title={showRing ? `${pctLabel} na faixa` : displayName}
        onClick={openProfile}
      >
        {showRing && (
          <CommissionRingSegments
            filledAmount={filledCurrentFaixa}
            escalao={escalao}
            segmentCount={1}
            animate={false}
          />
        )}
        <div className={`user-avatar ${showRing ? 'user-avatar-inset' : ''}`}>
          <UserAvatarInner
            photoUrl={photoUrl}
            displayName={displayName}
            collapsed
            showRing={showRing}
          />
        </div>
        {showRing && escalao != null && <EscalaoBadge variant="avatar" escalao={escalao} />}
      </button>
    </div>
  );
}

function foldViewAsName(value) {
  return String(value || '')
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .toLowerCase()
    .replace(/\s+/g, ' ')
    .trim();
}

function viewAsRowScore(member) {
  const email = String(member.email || member.Email || '').trim();
  const estado = String(member.estado || member.Estado || '');
  const active = /^(sim|ativo|admiss)/i.test(estado);
  return (active ? 4 : 0) + (email ? 2 : 0) + (/@grupohimo\.com$/i.test(email) ? 1 : 0);
}

function collapseViewAsDirectory(items) {
  const byName = new Map();
  for (const member of items || []) {
    const key = foldViewAsName(memberAssinatura(member));
    if (!key) continue;
    const prev = byName.get(key);
    if (!prev || viewAsRowScore(member) > viewAsRowScore(prev)) byName.set(key, member);
  }
  return [...byName.values()].sort((a, b) =>
    memberAssinatura(a).localeCompare(memberAssinatura(b), 'pt', { sensitivity: 'base' }));
}

function mapHimoColaboradorListItem(item) {
  const fields = getItemFields(item);
  const assinatura = String(
    fields.Assinatura || fields.assinatura || fields.Title || fields.Nome || fields.nome || ''
  ).trim();
  const email = String(
    fields.Email || fields.email || fields.CorreioEletronico || fields.Correio || fields.EMail || ''
  ).trim();
  const rawFuncao = fields.Fun_x00e7__x00e3_o || fields.Funcao || fields.funcao;
  const funcao = rawFuncao && typeof rawFuncao === 'object'
    ? String(rawFuncao.lookupValue || rawFuncao.LookupValue || rawFuncao.value || rawFuncao.Title || '')
    : String(rawFuncao || '');
  return {
    id: item.id || item.Id,
    Id: item.id || item.Id,
    assinatura,
    Assinatura: assinatura,
    email,
    Email: email,
    funcao,
    Funcao: funcao,
    fields,
  };
}

function ViewAsPickerModal({ open, members, loading, onClose, onConfirm, confirming }) {
  const [pickerSearch, setPickerSearch] = useState('');
  const [selected, setSelected] = useState(null);
  const [directory, setDirectory] = useState(null);
  const [dirLoading, setDirLoading] = useState(true);
  const searchRef = useRef(null);
  const dismiss = useOverlayDismiss(onClose);

  useEffect(() => {
    if (!open) {
      setPickerSearch('');
      setSelected(null);
      return undefined;
    }

    const prevOverflow = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    const onKey = (e) => {
      if (e.key === 'Escape') onClose();
    };
    window.addEventListener('keydown', onKey);
    // Focus search once the dialog is painted.
    requestAnimationFrame(() => searchRef.current?.focus());

    return () => {
      document.body.style.overflow = prevOverflow;
      window.removeEventListener('keydown', onKey);
    };
  }, [open, onClose]);

  useEffect(() => {
    if (!open) return undefined;
    let cancelled = false;
    setDirLoading(true);
    (async () => {
      try {
        const res = await apiFetch('/api/equipa/colaboradores/directory');
        const items = res.items || res.Items || [];
        if (!cancelled) setDirectory(collapseViewAsDirectory(Array.isArray(items) ? items : []));
      } catch {
        try {
          const raw = await fetchListItemsPages('himo-colaboradores', { pageSize: 200, maxPages: 25 });
          const mapped = raw.map(mapHimoColaboradorListItem).filter(m => m.assinatura);
          if (!cancelled) setDirectory(collapseViewAsDirectory(mapped));
        } catch {
          if (!cancelled) setDirectory(collapseViewAsDirectory(members || []));
        }
      } finally {
        if (!cancelled) setDirLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, [open]);

  if (!open) return null;

  const source = directory || [];
  const listLoading = dirLoading || directory == null;
  const filtered = source.filter(m => {
    const nome = (memberAssinatura(m) || '').toLowerCase();
    const email = (m.email || m.Email || '').toLowerCase();
    const q = pickerSearch.toLowerCase();
    return !q || nome.includes(q) || email.includes(q);
  });

  return ReactDOM.createPortal(
    <div
      className="view-as-overlay"
      role="presentation"
      {...dismiss}
    >
      <div
        className="view-as-dialog"
        role="dialog"
        aria-modal="true"
        aria-label="Selecione o colaborador"
        onClick={e => e.stopPropagation()}
      >
        <div className="view-as-header">
          <span />
          <h3>Selecione o colaborador</h3>
          <button type="button" className="view-as-close" onClick={onClose} aria-label="Fechar">
            <Icon name="close" size={18} color="#fff" />
          </button>
        </div>
        <div className="view-as-body">
          <input
            ref={searchRef}
            className="view-as-search"
            placeholder="Procurar por nome ou email"
            value={pickerSearch}
            onChange={e => setPickerSearch(e.target.value)}
          />
          <div className="view-as-list">
            {listLoading && <LoadingSpinner message="A carregar colaboradores..." />}
            {!listLoading && filtered.map(member => {
              const id = member.id || member.Id;
              const nome = memberAssinatura(member);
              const email = member.email || member.Email;
              const isSelected = selected?.id === id || selected?.Id === id;
              return (
                <button
                  key={id || email || nome}
                  type="button"
                  className={`view-as-row ${isSelected ? 'selected' : ''}`}
                  onClick={() => setSelected(member)}
                >
                  <span className="view-as-row-avatar">{userSigla(nome)}</span>
                  <span className="view-as-row-text">
                    <span className="view-as-row-name">{nome}</span>
                    <span className="view-as-row-email">{email || '—'}</span>
                  </span>
                </button>
              );
            })}
            {!listLoading && filtered.length === 0 && (
              <p className="empty-hint">Nenhum colaborador encontrado.</p>
            )}
          </div>
        </div>
        <div className="view-as-footer">
          <button type="button" className="btn btn-secondary" onClick={onClose}>Cancelar</button>
          <button
            type="button"
            className="btn btn-primary"
            disabled={!selected || confirming || !(selected.email || selected.Email)}
            onClick={() => selected && onConfirm(selected)}
          >
            {confirming ? 'A entrar...' : 'Entrar'}
          </button>
        </div>
      </div>
    </div>,
    document.body
  );
}

function CommissionRing({ percent, size = 44, stroke = 3 }) {
  const value = Math.min(Math.max(percent ?? 0, 0), 100);
  const color = window.MyHabitaTheme?.colors?.primary2 || '#2D9C48';
  const track = 'rgba(25, 24, 71, 0.12)';
  return (
    <div
      className="commission-ring"
      style={{
        width: size,
        height: size,
        background: `conic-gradient(${color} ${value * 3.6}deg, ${track} 0)`,
      }}
      title={`Comissão ${Math.round(value)}%`}
      aria-hidden="true"
    >
      <div className="commission-ring-inner" style={{ width: size - stroke * 2, height: size - stroke * 2 }} />
    </div>
  );
}

function Sidebar({ user, activePage, onNavigate, isLiveConnected, onOpenSettings, onOpenTheme, showThemeDebug, enabledModules, onLogout, requiresLogin, collapsed, isViewingAs, onExitViewAs, onOpenProfile }) {
  const navItems = NAV_ITEMS.map(item => ({
    ...item,
    disabled: !isNavItemEnabled(item, enabledModules),
  }));

  return (
    <aside className={`sidebar ${collapsed ? 'collapsed' : ''}`}>
      <div className="logo">
        <BrandLogo variant="myhabita" collapsed={collapsed} className="brand-logo-sidebar" />
      </div>
      <SidebarUserProfile
        user={user}
        collapsed={collapsed}
        isLiveConnected={isLiveConnected}
        isViewingAs={isViewingAs}
        onExitViewAs={onExitViewAs}
        onOpenProfile={onOpenProfile}
      />
      <nav className="sidebar-nav">
        {navItems.map(item => (
          <button
            key={item.id}
            type="button"
            className={`nav-item ${activePage === item.id ? 'active' : ''} ${item.disabled ? 'disabled' : ''}`}
            onClick={() => !item.disabled && onNavigate(item.id)}
            disabled={item.disabled}
            title={collapsed ? item.label : undefined}
          >
            <span className="nav-icon" aria-hidden="true"><Icon name={item.icon} size={20} /></span>
            <span className="nav-label">{item.label}</span>
          </button>
        ))}
      </nav>
      <div className="settings-btn">
        {showThemeDebug && (
          <button
            type="button"
            className={`nav-item nav-item-settings${activePage === 'theme' ? ' active' : ''}`}
            onClick={onOpenTheme}
            aria-label="Tema Nexora"
          >
            <span className="nav-icon" aria-hidden="true"><Icon name="palette" size={20} /></span>
            <span className="nav-label">Tema</span>
          </button>
        )}
        <button type="button" className="nav-item nav-item-settings" onClick={onOpenSettings}>
          <span className="nav-icon" aria-hidden="true"><Icon name="settings" size={20} /></span>
          <span className="nav-label">Definições</span>
        </button>
        {requiresLogin && (
          <button type="button" className="nav-item nav-item-logout" onClick={onLogout} title="Sair">
            <span className="nav-icon" aria-hidden="true"><Icon name="logout" size={20} /></span>
            <span className="nav-label">Sair</span>
          </button>
        )}
      </div>
      <div className="sidebar-footer">
        <BrandLogo variant="himo" className="brand-logo-himo-footer" />
      </div>
    </aside>
  );
}

function NotificationsPanel({ items, loading, error, onOpenAvaliacao, onOpenCalendar, calendarActionLabel = 'Abrir calendário', onOpenRelease, isAdmin, onSendLembretes, sendingLembretes }) {
  if (loading) return <CardLoading message="A carregar notificações…" />;
  if (error) return <p className="panel-empty-hint">{error}</p>;
  if (!items?.length) return <p className="panel-empty-hint">Sem notificações.</p>;
  return (
    <div className="notif-panel">
      <ul className="notif-list">
        {items.map(n => {
          const id = n.id || n.Id;
          const tipo = n.tipo || n.Tipo || '';
          const severity = n.severity || n.Severity || 'info';
          const requires = Boolean(n.requiresAction ?? n.RequiresAction);
          const isPhotoCancel = /^foto-cancelada-/i.test(String(id));
          return (
            <li key={id} className={`notif-item severity-${severity}`}>
              <strong>{n.titulo || n.Titulo}</strong>
              <p>{n.mensagem || n.Mensagem}</p>
              {(n.actionHint || n.ActionHint) && <span className="notif-hint">{n.actionHint || n.ActionHint}</span>}
              {requires && /avaliacao/i.test(tipo) && onOpenAvaliacao && (
                <button type="button" className="btn btn-primary btn-sm" onClick={onOpenAvaliacao}>
                  Abrir avaliação
                </button>
              )}
              {/fotografia|calendar/i.test(tipo) && onOpenCalendar && !isPhotoCancel && (
                <button type="button" className="btn btn-primary btn-sm" onClick={() => onOpenCalendar(n)}>
                  {calendarActionLabel}
                </button>
              )}
              {/release/i.test(tipo) && onOpenRelease && (
                <button type="button" className="btn btn-primary btn-sm" onClick={onOpenRelease}>
                  Ver novidades
                </button>
              )}
            </li>
          );
        })}
      </ul>
      {isAdmin && onSendLembretes && (
        <div className="notif-admin-actions">
          <button type="button" className="btn btn-secondary btn-sm" disabled={sendingLembretes} onClick={onSendLembretes}>
            {sendingLembretes ? 'A enviar…' : 'Enviar lembretes por email'}
          </button>
        </div>
      )}
    </div>
  );
}

/** Close overlay only when pointer down and up both happen on the mask — not after a drag from the panel. */
function useOverlayDismiss(onClose) {
  const startedOnMask = useRef(false);
  const onPointerDown = (e) => {
    if (e.button != null && e.button !== 0) return;
    startedOnMask.current = e.target === e.currentTarget;
  };
  const onPointerUp = (e) => {
    const shouldClose =
      startedOnMask.current &&
      e.target === e.currentTarget &&
      (e.button == null || e.button === 0);
    startedOnMask.current = false;
    if (shouldClose) onClose?.(e);
  };
  return { onPointerDown, onPointerUp };
}

/** Full-viewport modal shell (portaled so parent overflow/transform cannot clip it). */
function NegocioModalOverlay({ open, onClose, label, children, className = '' }) {
  const dismiss = useOverlayDismiss(onClose);
  if (!open) return null;
  return ReactDOM.createPortal(
    <div
      className={`negocio-modal-overlay${className ? ` ${className}` : ''}`}
      role="presentation"
      {...dismiss}
    >
      <div
        className="negocio-modal"
        role="dialog"
        aria-modal="true"
        aria-label={label || 'Diálogo'}
        onClick={e => e.stopPropagation()}
      >
        {children}
      </div>
    </div>,
    document.body
  );
}

function FilterDrawer({ open, onClose, title, children }) {
  const dismiss = useOverlayDismiss(onClose);
  if (!open) return null;
  return (
    <>
      <div className="filter-backdrop" {...dismiss} aria-hidden="true" />
      <aside className={`filter-drawer open`} role="dialog" aria-label={title || 'Filtros'}>
        <div className="panel-header">
          <h3>{title || 'Filtros'}</h3>
          <button type="button" className="btn-panel-close" onClick={onClose} aria-label="Fechar">
            <Icon name="close" size={18} color="#fff" />
          </button>
        </div>
        {children}
      </aside>
    </>
  );
}

/** In-app confirm (always asks — avoids blocked/skipped browser confirm()). */
function ConfirmDialog({
  open,
  title = 'Confirmar',
  message,
  confirmLabel = 'Confirmar',
  cancelLabel = 'Cancelar',
  danger = false,
  busy = false,
  onConfirm,
  onCancel,
}) {
  const dismiss = useOverlayDismiss(() => { if (!busy) onCancel?.(); });
  if (!open) return null;
  return ReactDOM.createPortal(
    <div
      className="confirm-dialog-overlay"
      role="presentation"
      {...dismiss}
    >
      <div
        className="confirm-dialog"
        role="alertdialog"
        aria-modal="true"
        aria-labelledby="confirm-dialog-title"
        aria-describedby="confirm-dialog-msg"
        onClick={e => e.stopPropagation()}
      >
        <h3 id="confirm-dialog-title">{title}</h3>
        <p id="confirm-dialog-msg">{message}</p>
        <div className="confirm-dialog-actions">
          <button type="button" className="btn btn-secondary" onClick={onCancel} disabled={busy}>
            {cancelLabel}
          </button>
          <button
            type="button"
            className={`btn ${danger ? 'btn-danger' : 'btn-primary'}`}
            onClick={onConfirm}
            disabled={busy}
            autoFocus
          >
            {busy ? 'A processar…' : confirmLabel}
          </button>
        </div>
      </div>
    </div>,
    document.body
  );
}

function ModuleTopbar({
  icon,
  title,
  user,
  actions,
  onToggleFilters,
  filtersOpen,
  showSearch = false,
  onOpenNotifications,
  notificationCount = 0,
  notificationsOpen = false,
  onCloseNotifications,
  notificationsPanel = null,
  dashboardGreeting = false,
  onNewDeal,
  headerExtra,
}) {
  const firstName = userAssinatura(user?.displayName).split(' ')[0] || 'Utilizador';
  const hour = new Date().getHours();
  const greeting = hour < 12 ? 'Bom dia' : hour < 18 ? 'Boa tarde' : 'Boa noite';
  const dateLabel = new Date().toLocaleDateString('pt-PT', { day: 'numeric', month: 'long', year: 'numeric' });
  const notifWrapRef = useRef(null);
  const notifPanelRef = useRef(null);
  const [notifPanelStyle, setNotifPanelStyle] = useState(null);

  const placeNotifPanel = useCallback(() => {
    const btn = notifWrapRef.current?.querySelector('.notif-bell-btn');
    if (!btn) return;
    const r = btn.getBoundingClientRect();
    const width = Math.min(360, window.innerWidth - 24);
    let left = r.right - width;
    if (left < 12) left = 12;
    setNotifPanelStyle({
      position: 'fixed',
      top: Math.round(r.bottom + 10),
      left: Math.round(left),
      width,
    });
  }, []);

  useEffect(() => {
    if (!notificationsOpen) {
      setNotifPanelStyle(null);
      return undefined;
    }
    placeNotifPanel();
    window.addEventListener('resize', placeNotifPanel);
    window.addEventListener('scroll', placeNotifPanel, true);
    return () => {
      window.removeEventListener('resize', placeNotifPanel);
      window.removeEventListener('scroll', placeNotifPanel, true);
    };
  }, [notificationsOpen, placeNotifPanel]);

  useEffect(() => {
    if (!notificationsOpen) return undefined;
    const onDoc = (e) => {
      const t = e.target;
      if (notifWrapRef.current?.contains(t) || notifPanelRef.current?.contains(t)) return;
      onCloseNotifications?.();
    };
    const onKey = (e) => {
      if (e.key === 'Escape') onCloseNotifications?.();
    };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDoc);
      document.removeEventListener('keydown', onKey);
    };
  }, [notificationsOpen, onCloseNotifications]);

  const notifBtn = onOpenNotifications && (
    <div className="notif-bell-wrap" ref={notifWrapRef}>
      <AppTooltip label={notificationsOpen ? '' : 'Notificações'}>
      <button
        type="button"
        className={`btn-icon-ghost notif-bell-btn${notificationsOpen ? ' is-open' : ''}`}
        aria-label="Notificações"
        aria-expanded={notificationsOpen}
        aria-haspopup="dialog"
        onClick={onOpenNotifications}
      >
        <Icon name="bell" size={18} />
        {notificationCount > 0 && (
          <span className="notif-bell-badge">{notificationCount > 9 ? '9+' : notificationCount}</span>
        )}
      </button>
      </AppTooltip>
      {notificationsOpen && ReactDOM.createPortal(
        <div
          ref={notifPanelRef}
          className="notif-float-panel"
          role="dialog"
          aria-label="Notificações"
          style={notifPanelStyle || { visibility: 'hidden' }}
        >
          <div className="notif-float-header">
            <h3>Notificações</h3>
            <button
              type="button"
              className="btn-icon-ghost"
              onClick={onCloseNotifications}
              aria-label="Fechar"
            >
              <Icon name="close" size={16} />
            </button>
          </div>
          {notificationsPanel}
        </div>,
        document.body
      )}
    </div>
  );

  if (dashboardGreeting) {
    return (
      <header className="topbar topbar-dashboard topbar-greeting">
        <div className="dashboard-greeting">
          <div className="topbar-title-row">
            <NavMenuButton />
            <h1 className="page-title">{greeting}, {firstName}! 👋</h1>
          </div>
          <p className="dashboard-greeting-sub">Aqui está o resumo do seu desempenho hoje.</p>
        </div>
        <HeaderConsultorMeter user={user} />
        <div className="dashboard-header-actions">
          {headerExtra}
          <span className="dashboard-date">
            <Icon name="calendar" size={14} />
            {dateLabel}
          </span>
          {notifBtn}
          {onNewDeal && <InserirMenu onPipeline={onNewDeal} />}
        </div>
      </header>
    );
  }

  return (
    <header className={`topbar topbar-module${showSearch ? ' topbar-dashboard' : ''}`}>
      <div className="topbar-left">
        <NavMenuButton />
        <h1 className="page-title">
          {icon ? (
            <span className="page-title-icon" aria-hidden="true">
              <Icon name={icon} size={22} />
            </span>
          ) : null}
          {title}
        </h1>
      </div>
      <div className="topbar-actions">
        {showSearch && (
          <label className="topbar-search">
            <Icon name="search" size={16} />
            <input type="search" placeholder="Pesquisar..." aria-label="Pesquisar" />
          </label>
        )}
        {actions}
        {notifBtn}
        {onToggleFilters && (
          <button
            type="button"
            className={`btn-filter-toggle${filtersOpen ? ' active' : ''}`}
            onClick={onToggleFilters}
            title="Filtros"
            aria-label="Filtros"
            aria-pressed={filtersOpen}
          >
            <Icon name="filter" size={18} />
          </button>
        )}
      </div>
    </header>
  );
}

function ModulePlaceholder({ icon, title, user }) {
  return (
    <>
      <ModuleTopbar icon={icon} title={title} user={user} />
      <div className="page-body">
        <div className="page-content module-layout">
          <div className="module-placeholder">
            <span className="module-placeholder-icon" aria-hidden="true"><Icon name={icon} size={48} /></span>
            <h2>{title}</h2>
            <p>Módulo em desenvolvimento. As listas estão configuradas nas definições.</p>
          </div>
        </div>
      </div>
    </>
  );
}

/** One rounded shell for tabs + toolbar + table — reuse on list pages. */
function ModulePageShell({ tabs = null, toolbar = null, children, className = '' }) {
  return (
    <div className={`module-page-shell${className ? ` ${className}` : ''}`}>
      {tabs ? <div className="module-page-shell-tabs">{tabs}</div> : null}
      {toolbar ? <div className="module-page-shell-toolbar">{toolbar}</div> : null}
      <div className="module-page-shell-body">{children}</div>
    </div>
  );
}

/** Admin-only shortcut from module toolbars into Definições → mapeamento de colunas. */
function MapearColunasToolbarButton({ user, listKey, onConfigureListColumns, label = 'Mapear colunas' }) {
  if (!userIsAdmin(user) || typeof onConfigureListColumns !== 'function' || !listKey) return null;
  return (
    <button
      type="button"
      className="btn btn-secondary"
      title="Mapear colunas da lista (Definições)"
      onClick={() => onConfigureListColumns(listKey)}
    >
      <Icon name="kanban" size={15} />
      {label}
    </button>
  );
}

function DashboardFilterPanel({ filters, onChange, onApply, compact = false }) {
  const meses = ['Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'];
  const currentYear = new Date().getFullYear();
  const yearOptions = [...new Set([2024, 2025, 2026, 2027, currentYear - 1, currentYear, currentYear + 1])]
    .filter(y => Number.isFinite(y))
    .sort((a, b) => a - b);
  return (
    <>
      <div className="form-group">
        <label>Ano</label>
        <AppSelect
          value={filters.ano}
          options={yearOptions.map(y => ({ value: y, label: String(y) }))}
          onChange={v => onChange({ ...filters, ano: parseInt(v, 10) })}
          ariaLabel="Ano"
        />
      </div>
      <div className="form-group">
        <label>Mês</label>
        <AppSelect
          value={filters.mes}
          options={meses.map((m, i) => ({ value: i + 1, label: m }))}
          onChange={v => onChange({ ...filters, mes: parseInt(v, 10) })}
          ariaLabel="Mês"
        />
      </div>
      <div className="checkbox-group">
        <label>Natureza</label>
        {['Arrendamento', 'Venda'].map(n => (
          <label key={n}>
            <input
              type="checkbox"
              checked={filters.naturezas.includes(n)}
              onChange={e => {
                const naturezas = e.target.checked
                  ? [...filters.naturezas, n]
                  : filters.naturezas.filter(x => x !== n);
                onChange({ ...filters, naturezas });
              }}
            />
            {n}
          </label>
        ))}
      </div>
      <div className="checkbox-group">
        <label>Estado</label>
        {['Concluído', 'Perdido'].map(s => (
          <label key={s}>
            <input
              type="checkbox"
              checked={filters.estados.includes(s)}
              onChange={e => {
                const estados = e.target.checked
                  ? [...filters.estados, s]
                  : filters.estados.filter(x => x !== s);
                onChange({ ...filters, estados });
              }}
            />
            {s}
          </label>
        ))}
      </div>
      <button
        type="button"
        className="btn btn-primary"
        style={compact ? { width: '100%' } : { width: 'calc(100% - 2rem)', margin: '0 1rem 1rem' }}
        onClick={onApply}
      >
        Aplicar filtros
      </button>
    </>
  );
}

function FilterPanel({ filters, onChange, onApply }) {
  return (
    <div className="side-panel filtros">
      <div className="panel-header">
        <span className="panel-header-icon" aria-hidden="true"><Icon name="search" size={18} /></span>
        <h3>Filtros</h3>
      </div>
      <div className="form-group">
        <label>Operação</label>
        <AppSelect
          value={filters.operacao}
          emptyOption="Todas"
          options={['Porto', 'Lisboa', 'Alentejo', 'Sines']}
          onChange={v => onChange({ ...filters, operacao: v })}
          ariaLabel="Operação"
        />
      </div>
      <div className="form-group">
        <label>Natureza de negócio</label>
        <AppSelect
          value={filters.naturezaNegocio}
          emptyOption="Todas"
          options={['Venda', 'Arrendamento']}
          onChange={v => onChange({ ...filters, naturezaNegocio: v })}
          ariaLabel="Natureza de negócio"
        />
      </div>
      <div className="form-group">
        <label>Probabilidade</label>
        <AppSelect
          value={filters.probabilidade}
          emptyOption="Todas"
          options={['Alta', 'Média', 'Baixa']}
          onChange={v => onChange({ ...filters, probabilidade: v })}
          ariaLabel="Probabilidade"
        />
      </div>
      <div className="form-group">
        <label>Estado</label>
        <AppSelect
          value={filters.estado}
          emptyOption="Todos"
          options={['Em Curso', 'Concluído', 'Perdido']}
          onChange={v => onChange({ ...filters, estado: v })}
          ariaLabel="Estado"
        />
      </div>
      <button className="btn btn-primary" style={{ width: '100%' }} onClick={onApply}>Aplicar filtros</button>
    </div>
  );
}

function UpdatePanel({ item, onSave, onClose, saving }) {
  const [form, setForm] = useState({
    estado: item?.estado || 'Em Curso',
    comissaoTotalHabita: item?.comissaoTotalHabita ?? '',
    probabilidade: item?.probabilidade || 'Alta',
    dataPrevistaFaturacao: item?.dataPrevistaFaturacao?.slice(0, 10) || '',
  });

  useEffect(() => {
    setForm({
      estado: item?.estado || 'Em Curso',
      comissaoTotalHabita: item?.comissaoTotalHabita ?? '',
      probabilidade: item?.probabilidade || 'Alta',
      dataPrevistaFaturacao: item?.dataPrevistaFaturacao?.slice(0, 10) || '',
    });
  }, [item]);

  if (!item) return null;

  const derived = dateDerivedParts(form.dataPrevistaFaturacao);

  const handleSave = () => {
    onSave({
      estado: form.estado,
      comissaoTotalHabita: form.comissaoTotalHabita ? parseFloat(form.comissaoTotalHabita) : null,
      probabilidade: form.probabilidade,
      dataPrevistaFaturacao: form.dataPrevistaFaturacao ? new Date(form.dataPrevistaFaturacao).toISOString() : null,
    });
  };

  return (
    <div className="negocio-modal-panel">
      <div className="negocio-modal-header">
        <div>
          <h3>Atualizar negócio</h3>
          <p className="negocio-modal-subtitle">{item.referencia || 'Sem referência'}</p>
        </div>
        <button type="button" className="negocio-modal-close" onClick={onClose} aria-label="Fechar">
          <Icon name="close" size={18} />
        </button>
      </div>
      <div className="negocio-modal-body">
        <div className="form-group">
          <label>Estado</label>
          <div className="segmented-control" role="radiogroup" aria-label="Estado">
            {['Em Curso', 'Concluído', 'Perdido'].map(opt => (
              <button
                key={opt}
                type="button"
                className={`segmented-option${form.estado === opt ? ' active' : ''}`}
                onClick={() => setForm({ ...form, estado: opt })}
                aria-pressed={form.estado === opt}
              >
                {opt}
              </button>
            ))}
          </div>
        </div>
        <div className="negocio-modal-grid">
          <div className="form-group">
            <label>Comissão Total HABITA</label>
            <NumericInputField
              kind="money"
              label="Comissão Total HABITA"
              value={form.comissaoTotalHabita}
              onChange={next => setForm({ ...form, comissaoTotalHabita: next })}
            />
          </div>
          <div className="form-group">
            <label>Probabilidade</label>
            <AppSelect
              value={form.probabilidade}
              options={['Alta', 'Média', 'Baixa']}
              onChange={v => setForm({ ...form, probabilidade: v })}
              ariaLabel="Probabilidade"
            />
          </div>
        </div>
        <div className="form-group">
          <label>Faturação (Data Prevista)</label>
          <DatePickerField
            value={form.dataPrevistaFaturacao}
            onChange={v => setForm({ ...form, dataPrevistaFaturacao: v })}
          />
        </div>
        <div className="negocio-modal-grid">
          <div className="form-group">
            <label>Ano</label>
            <input type="text" value={derived ? derived.ano : '—'} readOnly disabled />
          </div>
          <div className="form-group">
            <label>Mês</label>
            <input type="text" value={derived ? derived.mes : '—'} readOnly disabled />
          </div>
          <div className="form-group">
            <label>Nº Semana</label>
            <input type="text" value={derived ? derived.numeroSemana : '—'} readOnly disabled />
          </div>
        </div>
      </div>
      <div className="negocio-modal-footer">
        <button type="button" className="btn btn-secondary" onClick={onClose}>Cancelar</button>
        <button type="button" className="btn btn-primary" onClick={handleSave} disabled={saving}>
          {saving ? 'A guardar...' : 'Guardar'}
        </button>
      </div>
    </div>
  );
}

const WEEKDAYS_PT = ['Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb', 'Dom'];
const MONTHS_PT = [
  'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho',
  'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro',
];

function parseIsoDateOnly(value) {
  if (!value || typeof value !== 'string') return null;
  const m = value.trim().match(/^(\d{4})-(\d{2})-(\d{2})/);
  if (!m) return null;
  const y = Number(m[1]);
  const mo = Number(m[2]);
  const d = Number(m[3]);
  const dt = new Date(y, mo - 1, d);
  if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
  return dt;
}

function toIsoDateOnly(date) {
  if (!(date instanceof Date) || Number.isNaN(date.getTime())) return '';
  const y = date.getFullYear();
  const m = String(date.getMonth() + 1).padStart(2, '0');
  const d = String(date.getDate()).padStart(2, '0');
  return `${y}-${m}-${d}`;
}

/** ISO-8601 week number (Monday-based), matching .NET ISOWeek.GetWeekOfYear. */
function getIsoWeekNumber(date) {
  const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
  const dayNum = d.getUTCDay() || 7;
  d.setUTCDate(d.getUTCDate() + 4 - dayNum);
  const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
  return Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
}

/** From yyyy-mm-dd → { ano, mes (1–12), numeroSemana }. */
function dateDerivedParts(isoOrDate) {
  const dt = typeof isoOrDate === 'string'
    ? parseIsoDateOnly(isoOrDate)
    : (isoOrDate instanceof Date && !Number.isNaN(isoOrDate.getTime()) ? isoOrDate : null);
  if (!dt) return null;
  return {
    ano: dt.getFullYear(),
    mes: dt.getMonth() + 1,
    numeroSemana: getIsoWeekNumber(dt),
  };
}

function anoMesSemanaKind(fieldKey, label) {
  const k = String(fieldKey || '').toLowerCase();
  const lb = String(label || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').trim();
  if (k === 'ano' || lb === 'ano') return 'ano';
  if (k === 'm_x00ea_s' || k === 'mes' || lb === 'mes') return 'mes';
  if (k.includes('semana') || lb.includes('semana')) return 'semana';
  return null;
}

function applyDateDerivedToForm(form, columns, isoDate) {
  const parts = dateDerivedParts(isoDate);
  if (!parts || !columns) return form;
  const next = { ...form };
  let changed = false;
  Object.entries(columns).forEach(([label, key]) => {
    const kind = anoMesSemanaKind(key, label);
    if (!kind) return;
    const val = kind === 'ano' ? String(parts.ano)
      : kind === 'mes' ? String(parts.mes)
      : String(parts.numeroSemana);
    if (next[key] !== val) {
      next[key] = val;
      changed = true;
    }
  });
  return changed ? next : form;
}

/** Prefer Data, then faturação/conclusão, then first filled date field. */
function pickPrimaryDateIso(form, columns) {
  if (!form || !columns) return '';
  const entries = Object.entries(columns);
  const byKind = (pred) => {
    for (const [label, key] of entries) {
      if (!isDateFieldKey(key) && !isDateFieldKey(label)) continue;
      if (!pred(String(key).toLowerCase(), String(label).toLowerCase())) continue;
      if (form[key]) return form[key];
    }
    return '';
  };
  return byKind((k) => k === 'data')
    || byKind((k, lb) => k.includes('fatur') || k.includes('efetiv') || k.includes('conclu') || lb.includes('fatur'))
    || byKind(() => true)
    || form.dataPrevistaFaturacao
    || '';
}

function formatDatePt(value) {
  const dt = parseIsoDateOnly(value);
  if (!dt) return '';
  return dt.toLocaleDateString('pt-PT', { day: '2-digit', month: '2-digit', year: 'numeric' });
}

function pad2(n) {
  return String(n).padStart(2, '0');
}

function formatHmFromDate(d) {
  if (!(d instanceof Date) || Number.isNaN(d.getTime())) return '';
  return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
}

function parseTimeHm(value) {
  const m = String(value || '').trim().match(/^(\d{1,2}):(\d{2})(?::\d{2})?/);
  if (!m) return null;
  const h = Number(m[1]);
  const min = Number(m[2]);
  if (!Number.isFinite(h) || !Number.isFinite(min) || h < 0 || h > 23 || min < 0 || min > 59) return null;
  return { h, m: min };
}

function formatTimePt(value) {
  const p = parseTimeHm(value);
  if (!p) return '';
  return `${pad2(p.h)}:${pad2(p.m)}`;
}

function pickerAnchorStyle(triggerEl, width, panelH) {
  if (!triggerEl) {
    return { position: 'fixed', top: 0, left: 0, visibility: 'hidden', pointerEvents: 'none' };
  }
  const r = triggerEl.getBoundingClientRect();
  const w = Math.min(width, window.innerWidth - 24);
  let left = r.left;
  if (left + w > window.innerWidth - 12) left = window.innerWidth - w - 12;
  if (left < 12) left = 12;
  const spaceBelow = window.innerHeight - r.bottom;
  const openUp = spaceBelow < Math.min(panelH, 280) && r.top > spaceBelow;
  return {
    position: 'fixed',
    top: openUp ? undefined : Math.round(r.bottom + 6),
    bottom: openUp ? Math.round(window.innerHeight - r.top + 6) : undefined,
    left: Math.round(left),
    width: w,
    zIndex: 1600,
  };
}

function usePickerPanel(open, triggerRef, panelRef, width, panelH) {
  const [panelStyle, setPanelStyle] = useState(null);
  const placePanel = useCallback(() => {
    setPanelStyle(pickerAnchorStyle(triggerRef.current, width, panelH));
  }, [width, panelH]);

  useLayoutEffect(() => {
    if (!open) return undefined;
    placePanel();
    const onWin = () => placePanel();
    const onScroll = (e) => {
      if (panelRef.current && (e.target === panelRef.current || panelRef.current.contains(e.target))) return;
      placePanel();
    };
    window.addEventListener('resize', onWin);
    window.addEventListener('scroll', onScroll, true);
    return () => {
      window.removeEventListener('resize', onWin);
      window.removeEventListener('scroll', onScroll, true);
    };
  }, [open, placePanel, panelRef]);

  const prepareOpen = useCallback(() => {
    setPanelStyle(pickerAnchorStyle(triggerRef.current, width, panelH));
  }, [width, panelH]);

  return { panelStyle: panelStyle || pickerAnchorStyle(null, width, panelH), prepareOpen };
}

function formatDateTimePt(value, withYear = true) {
  const d = value instanceof Date ? value : new Date(value);
  if (Number.isNaN(d.getTime())) return '—';
  const datePart = withYear
    ? d.toLocaleDateString('pt-PT', { day: '2-digit', month: '2-digit', year: 'numeric' })
    : d.toLocaleDateString('pt-PT', { day: '2-digit', month: '2-digit' });
  return `${datePart} ${formatHmFromDate(d)}`;
}

/** Value is always `yyyy-mm-dd` (or '') — same contract as `<input type="date">`. Display is always `dd/mm/aaaa`. */
function DatePickerField({ value = '', onChange, placeholder = 'dd/mm/aaaa', disabled = false, id, min, max }) {
  const [open, setOpen] = useState(false);
  const [view, setView] = useState(() => parseIsoDateOnly(value) || new Date());
  const wrapRef = useRef(null);
  const panelRef = useRef(null);
  const triggerRef = useRef(null);
  const { panelStyle, prepareOpen } = usePickerPanel(open, triggerRef, panelRef, 320, 360);
  const selected = parseIsoDateOnly(value);
  const today = new Date();
  today.setHours(0, 0, 0, 0);

  useEffect(() => {
    const dt = parseIsoDateOnly(value);
    if (dt) setView(dt);
  }, [value]);

  useEffect(() => {
    if (!open) return undefined;
    const onDoc = (e) => {
      const t = e.target;
      if (wrapRef.current?.contains(t) || panelRef.current?.contains(t)) return;
      if (t.closest?.('.app-select-popover')) return;
      setOpen(false);
    };
    const onKey = (e) => {
      if (e.key === 'Escape') {
        e.stopImmediatePropagation();
        setOpen(false);
      }
    };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDoc);
      document.removeEventListener('keydown', onKey);
    };
  }, [open]);

  const year = view.getFullYear();
  const month = view.getMonth();
  const nowYear = today.getFullYear();
  const yearFrom = Math.min(nowYear - 12, year - 2);
  const yearTo = Math.max(nowYear + 15, year + 2);
  const yearOptions = [];
  for (let y = yearFrom; y <= yearTo; y++) yearOptions.push(y);

  const first = new Date(year, month, 1);
  // Monday-first grid
  const startPad = (first.getDay() + 6) % 7;
  const daysInMonth = new Date(year, month + 1, 0).getDate();
  const cells = [];
  for (let i = 0; i < startPad; i++) cells.push(null);
  for (let d = 1; d <= daysInMonth; d++) cells.push(new Date(year, month, d));
  while (cells.length % 7 !== 0) cells.push(null);

  const pick = (dt) => {
    const iso = toIsoDateOnly(dt);
    if (min && iso < min) return;
    if (max && iso > max) return;
    onChange?.(iso);
    setOpen(false);
  };

  const setMonthYear = (nextMonth, nextYear) => {
    setView(new Date(nextYear, nextMonth, 1));
  };

  const display = formatDatePt(value) || '';

  return (
    <div className={`date-picker${disabled ? ' is-disabled' : ''}${open ? ' is-open' : ''}`} ref={wrapRef}>
      <button
        type="button"
        id={id}
        ref={triggerRef}
        className="date-picker-trigger"
        disabled={disabled}
        aria-haspopup="dialog"
        aria-expanded={open}
        onClick={() => {
          if (disabled) return;
          if (open) {
            setOpen(false);
            return;
          }
          prepareOpen();
          setOpen(true);
        }}
      >
        <span className={display ? 'date-picker-value' : 'date-picker-placeholder'}>
          {display || placeholder}
        </span>
        <span className="date-picker-actions" aria-hidden="true">
          {value && !disabled && (
            <AppTooltip label="Limpar">
              <span
                className="date-picker-clear"
                role="presentation"
                onClick={(e) => {
                  e.stopPropagation();
                  onChange?.('');
                }}
              >
                <Icon name="close" size={12} />
              </span>
            </AppTooltip>
          )}
          <Icon name="calendar" size={16} />
        </span>
      </button>
      {open && ReactDOM.createPortal(
        <div
          ref={panelRef}
          className="date-picker-panel"
          role="dialog"
          aria-label="Escolher data"
          style={panelStyle}
        >
          <div className="date-picker-nav">
            <button
              type="button"
              className="date-picker-nav-btn"
              aria-label="Mês anterior"
              onClick={() => setMonthYear(month - 1, year)}
            >
              ‹
            </button>
            <div className="date-picker-selectors">
              <label className="date-picker-select-wrap">
                <span className="sr-only">Mês</span>
                <AppSelect
                  variant="compact"
                  className="date-picker-select-month"
                  value={month}
                  ariaLabel="Mês"
                  showMenuTitle={false}
                  layout="list"
                  menuWidth={168}
                  options={MONTHS_PT.map((name, i) => ({ value: i, label: name }))}
                  onChange={v => setMonthYear(Number(v), year)}
                />
              </label>
              <label className="date-picker-select-wrap">
                <span className="sr-only">Ano</span>
                <AppSelect
                  variant="compact"
                  className="date-picker-select-year"
                  value={year}
                  ariaLabel="Ano"
                  showMenuTitle={false}
                  layout="list"
                  menuWidth={108}
                  options={yearOptions.map(y => ({ value: y, label: String(y) }))}
                  onChange={v => setMonthYear(month, Number(v))}
                />
              </label>
            </div>
            <button
              type="button"
              className="date-picker-nav-btn"
              aria-label="Mês seguinte"
              onClick={() => setMonthYear(month + 1, year)}
            >
              ›
            </button>
          </div>
          <div className="date-picker-weekdays">
            {WEEKDAYS_PT.map(d => <span key={d}>{d}</span>)}
          </div>
          <div className="date-picker-grid">
            {cells.map((dt, i) => {
              if (!dt) return <span key={`e-${i}`} className="date-picker-day is-empty" />;
              const iso = toIsoDateOnly(dt);
              const isSelected = selected && toIsoDateOnly(selected) === iso;
              const isToday = toIsoDateOnly(today) === iso;
              const outOfRange = (min && iso < min) || (max && iso > max);
              return (
                <button
                  key={iso}
                  type="button"
                  disabled={outOfRange}
                  className={`date-picker-day${isSelected ? ' is-selected' : ''}${isToday ? ' is-today' : ''}${outOfRange ? ' is-disabled' : ''}`}
                  onMouseDown={(e) => e.preventDefault()}
                  onClick={() => pick(dt)}
                >
                  {dt.getDate()}
                </button>
              );
            })}
          </div>
          <div className="date-picker-footer">
            <button
              type="button"
              className="btn btn-secondary btn-sm"
              disabled={(min && toIsoDateOnly(today) < min) || (max && toIsoDateOnly(today) > max)}
              onClick={() => pick(today)}
            >
              Hoje
            </button>
            <button
              type="button"
              className="btn btn-secondary btn-sm"
              onClick={() => { onChange?.(''); setOpen(false); }}
            >
              Limpar
            </button>
          </div>
        </div>,
        document.body
      )}
    </div>
  );
}

/** Value is always `HH:mm` (or '') — same contract as `<input type="time">`. Display is always 24h. */
function TimePickerField({ value = '', onChange, placeholder = 'hh:mm', disabled = false, id, min, max, step = 5 }) {
  const [open, setOpen] = useState(false);
  const wrapRef = useRef(null);
  const panelRef = useRef(null);
  const triggerRef = useRef(null);
  const { panelStyle, prepareOpen } = usePickerPanel(open, triggerRef, panelRef, 232, 300);
  const parsed = parseTimeHm(value);
  const hour = parsed ? parsed.h : null;
  const minute = parsed ? parsed.m : null;
  const minHm = min ? formatTimePt(min) : '';
  const maxHm = max ? formatTimePt(max) : '';

  useEffect(() => {
    if (!open) return undefined;
    const onDoc = (e) => {
      const t = e.target;
      if (wrapRef.current?.contains(t) || panelRef.current?.contains(t)) return;
      setOpen(false);
    };
    const onKey = (e) => {
      if (e.key === 'Escape') {
        e.stopImmediatePropagation();
        setOpen(false);
      }
    };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDoc);
      document.removeEventListener('keydown', onKey);
    };
  }, [open]);

  useLayoutEffect(() => {
    if (!open || !panelRef.current) return;
    panelRef.current.querySelectorAll('.time-picker-list').forEach((list) => {
      const sel = list.querySelector('.time-picker-opt.is-selected');
      if (!sel) return;
      const top = sel.offsetTop - (list.clientHeight - sel.offsetHeight) / 2;
      list.scrollTop = Math.max(0, top);
    });
  }, [open]);

  const stepN = Number(step) > 0 ? Number(step) : 5;
  const minutes = [];
  for (let m = 0; m < 60; m += stepN) minutes.push(m);
  if (minute != null && !minutes.includes(minute)) {
    minutes.push(minute);
    minutes.sort((a, b) => a - b);
  }

  const isOut = (hm) => (minHm && hm < minHm) || (maxHm && hm > maxHm);
  const hourDisabled = (h) => (minHm && `${pad2(h)}:59` < minHm) || (maxHm && `${pad2(h)}:00` > maxHm);
  const minuteDisabled = (h, m) => isOut(`${pad2(h)}:${pad2(m)}`);

  const commit = (h, m, close) => {
    const hm = `${pad2(h)}:${pad2(m)}`;
    if (isOut(hm)) return;
    onChange?.(hm);
    if (close) setOpen(false);
  };

  const pickHour = (h) => {
    const nextM = minute != null ? minute : 0;
    const hm = `${pad2(h)}:${pad2(nextM)}`;
    if (!isOut(hm)) {
      commit(h, nextM, false);
      return;
    }
    const fallback = minutes.find(m => !minuteDisabled(h, m));
    if (fallback != null) commit(h, fallback, false);
  };

  const now = new Date();
  const nowHm = formatHmFromDate(now);
  const display = formatTimePt(value) || '';

  return (
    <div className={`date-picker time-picker${disabled ? ' is-disabled' : ''}${open ? ' is-open' : ''}`} ref={wrapRef}>
      <button
        type="button"
        id={id}
        ref={triggerRef}
        className="date-picker-trigger"
        disabled={disabled}
        aria-haspopup="dialog"
        aria-expanded={open}
        onClick={() => {
          if (disabled) return;
          if (open) {
            setOpen(false);
            return;
          }
          prepareOpen();
          setOpen(true);
        }}
      >
        <span className={display ? 'date-picker-value' : 'date-picker-placeholder'}>
          {display || placeholder}
        </span>
        <span className="date-picker-actions" aria-hidden="true">
          {value && !disabled && (
            <AppTooltip label="Limpar">
              <span
                className="date-picker-clear"
                role="presentation"
                onClick={(e) => {
                  e.stopPropagation();
                  onChange?.('');
                }}
              >
                <Icon name="close" size={12} />
              </span>
            </AppTooltip>
          )}
          <Icon name="clock" size={16} />
        </span>
      </button>
      {open && ReactDOM.createPortal(
        <div
          ref={panelRef}
          className="date-picker-panel time-picker-panel"
          role="dialog"
          aria-label="Escolher hora"
          style={panelStyle}
        >
          <div className="time-picker-cols">
            <div>
              <span className="time-picker-col-label">Horas</span>
              <div className="time-picker-list" role="listbox" aria-label="Horas">
                {Array.from({ length: 24 }, (_, h) => {
                  const blocked = hourDisabled(h);
                  return (
                    <button
                      key={h}
                      type="button"
                      role="option"
                      aria-selected={hour === h}
                      disabled={blocked}
                      className={`time-picker-opt${hour === h ? ' is-selected' : ''}${blocked ? ' is-disabled' : ''}`}
                      onMouseDown={(e) => e.preventDefault()}
                      onClick={() => pickHour(h)}
                    >
                      {pad2(h)}
                    </button>
                  );
                })}
              </div>
            </div>
            <div>
              <span className="time-picker-col-label">Minutos</span>
              <div className="time-picker-list" role="listbox" aria-label="Minutos">
                {minutes.map((m) => {
                  const h = hour != null ? hour : 9;
                  const blocked = minuteDisabled(h, m);
                  return (
                    <button
                      key={m}
                      type="button"
                      role="option"
                      aria-selected={minute === m}
                      disabled={blocked}
                      className={`time-picker-opt${minute === m ? ' is-selected' : ''}${blocked ? ' is-disabled' : ''}`}
                      onMouseDown={(e) => e.preventDefault()}
                      onClick={() => commit(h, m, true)}
                    >
                      {pad2(m)}
                    </button>
                  );
                })}
              </div>
            </div>
          </div>
          <div className="date-picker-footer">
            <button
              type="button"
              className="btn btn-secondary btn-sm"
              disabled={isOut(nowHm)}
              onClick={() => commit(now.getHours(), now.getMinutes(), true)}
            >
              Agora
            </button>
            <button
              type="button"
              className="btn btn-secondary btn-sm"
              onClick={() => { onChange?.(''); setOpen(false); }}
            >
              Limpar
            </button>
          </div>
        </div>,
        document.body
      )}
    </div>
  );
}

function endOfMonthIso(date = new Date()) {
  return toIsoDateOnly(new Date(date.getFullYear(), date.getMonth() + 1, 0));
}

function pickDefaultOperacaoLabel(options, user) {
  const opts = options || [];
  const allowed = user?.allowedOperacoes || user?.AllowedOperacoes || [];
  const local = user?.local || user?.Local || '';
  const needles = [
    ...(Array.isArray(allowed) ? allowed : []),
    local,
  ].map(s => String(s || '').trim().toLowerCase()).filter(Boolean);

  for (const needle of needles) {
    const exact = opts.find(o => String(o.label).toLowerCase() === needle);
    if (exact) return exact.label;
  }
  for (const needle of needles) {
    const soft = opts.find(o => {
      const label = String(o.label).toLowerCase();
      return label.includes(needle) || needle.includes(label);
    });
    if (soft) return soft.label;
  }
  return opts[0]?.label || '';
}

function pickDefaultOperacaoOption(options, user) {
  const opts = options || [];
  if (!opts.length) return null;
  const label = pickDefaultOperacaoLabel(opts, user);
  return opts.find(o => String(o.label).toLowerCase() === String(label).toLowerCase()) || opts[0];
}

async function resolveSelfConsultorPrefill(user) {
  const email = String(user?.email || user?.Email || '').trim();
  if (!email) return null;
  try {
    const params = new URLSearchParams({ take: '8', search: email });
    const result = await apiFetch(`/api/equipa/consultores?${params}`);
    const list = Array.isArray(result) ? result : (result.items || result.Items || []);
    const mapped = list.map(h => ({
      lookupId: String(h.lookupId ?? h.LookupId ?? ''),
      displayName: h.displayName || h.DisplayName || '',
      email: h.email || h.Email || '',
    })).filter(h => h.lookupId && h.lookupId !== '0');
    const hit = mapped.find(h => String(h.email).toLowerCase() === email.toLowerCase()) || mapped[0];
    if (!hit) return null;
    return {
      consultorLookupId: hit.lookupId,
      consultorLabel: hit.displayName || email,
    };
  } catch {
    return null;
  }
}

function CreateNegocioPanel({ onCreate, onClose, saving, user }) {
  const isAdmin = userIsAdmin(user);
  const canChangeConsultor = userCanChangeConsultor(user);
  const [form, setForm] = useState({
    referencia: '',
    operacao: '',
    estado: 'Em Curso',
    naturezaNegocio: 'Venda',
    probabilidade: 'Alta',
    comissaoTotalHabita: '',
    dataPrevistaFaturacao: endOfMonthIso(),
    consultorLookupId: '',
    consultorLabel: '',
  });
  const [operacaoOptions, setOperacaoOptions] = useState([]);
  const prefilledRef = useRef(false);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      const opts = await loadOperacaoOptions(user);
      if (cancelled) return;
      setOperacaoOptions(opts);
      setForm(prev => ({
        ...prev,
        operacao: prev.operacao || pickDefaultOperacaoLabel(opts, user),
        dataPrevistaFaturacao: prev.dataPrevistaFaturacao || endOfMonthIso(),
      }));

      if (prefilledRef.current) return;
      prefilledRef.current = true;
      const self = await resolveSelfConsultorPrefill(user);
      if (cancelled || !self) return;
      setForm(prev => {
        // Consultors always stay on self; admins keep a manual pick if already set.
        if (prev.consultorLookupId && canChangeConsultor) return prev;
        return {
          ...prev,
          consultorLookupId: self.consultorLookupId,
          consultorLabel: self.consultorLabel,
        };
      });
    })();
    return () => { cancelled = true; };
  }, [user, canChangeConsultor]);

  const derived = dateDerivedParts(form.dataPrevistaFaturacao);
  const operacaoLocked = !isAdmin && operacaoOptions.length <= 1;

  const handleCreate = () => {
    if (!form.referencia.trim()) {
      toast.warning('Indique a referência.');
      return;
    }
    if (!form.consultorLookupId) {
      toast.warning('Consultor não identificado. Peça a um administrador para verificar o seu perfil.');
      return;
    }
    if (!form.operacao) {
      toast.warning('Selecione a operação.');
      return;
    }
    const payload = {
      referencia: form.referencia.trim(),
      operacao: form.operacao || null,
      estado: form.estado,
      naturezaNegocio: form.naturezaNegocio || null,
      probabilidade: form.probabilidade,
      comissaoTotalHabita: form.comissaoTotalHabita ? parseFloat(form.comissaoTotalHabita) : null,
      dataPrevistaFaturacao: form.dataPrevistaFaturacao ? new Date(form.dataPrevistaFaturacao).toISOString() : null,
      consultorLookupId: parseInt(form.consultorLookupId, 10),
    };
    onCreate(payload);
  };

  const opSelectOptions = operacaoOptions.length
    ? operacaoOptions
    : ['Porto', 'Lisboa', 'Alentejo', 'Sines', 'Algarve', 'Oeste'].map(label => ({ id: label, label }));

  return (
    <div className="negocio-modal-panel">
      <div className="negocio-modal-header">
        <div>
          <h3>Novo negócio</h3>
          <p className="negocio-modal-subtitle">Preencha os dados do negócio no Pipeline</p>
        </div>
        <button type="button" className="negocio-modal-close" onClick={onClose} aria-label="Fechar">
          <Icon name="close" size={18} />
        </button>
      </div>
      <div className="negocio-modal-body">
        <div className="form-group">
          <label>Referência</label>
          <input
            value={form.referencia}
            onChange={e => setForm({ ...form, referencia: e.target.value })}
            placeholder="Ex: T1 Civilria"
            autoFocus
          />
        </div>
        <div className="form-group">
          <label>Consultor</label>
          <ConsultorSearchField
            value={form.consultorLookupId}
            labelText={form.consultorLabel}
            disabled={!canChangeConsultor}
            onChange={({ lookupId, label }) => {
              if (!canChangeConsultor) return;
              setForm({
                ...form,
                consultorLookupId: lookupId,
                consultorLabel: label || '',
              });
            }}
          />
        </div>
        <div className="negocio-modal-grid">
          <div className="form-group">
            <label>Operação</label>
            <AppSelect
              value={form.operacao}
              disabled={operacaoLocked}
              emptyOption={form.operacao ? undefined : '—'}
              options={opSelectOptions.map(o => ({ value: o.label, label: o.label }))}
              onChange={v => setForm({ ...form, operacao: v })}
              ariaLabel="Operação"
            />
          </div>
          <div className="form-group">
            <label>Natureza</label>
            <AppSelect
              value={form.naturezaNegocio}
              options={['Venda', 'Arrendamento']}
              onChange={v => setForm({ ...form, naturezaNegocio: v })}
              ariaLabel="Natureza"
            />
          </div>
        </div>
        <div className="form-group">
          <label>Estado</label>
          <div className="segmented-control" role="radiogroup" aria-label="Estado">
            {['Em Curso', 'Concluído', 'Perdido'].map(opt => (
              <button
                key={opt}
                type="button"
                className={`segmented-option${form.estado === opt ? ' active' : ''}`}
                onClick={() => setForm({ ...form, estado: opt })}
                aria-pressed={form.estado === opt}
              >
                {opt}
              </button>
            ))}
          </div>
        </div>
        <div className="negocio-modal-grid">
          <div className="form-group">
            <label>Probabilidade</label>
            <AppSelect
              value={form.probabilidade}
              options={['Alta', 'Média', 'Baixa']}
              onChange={v => setForm({ ...form, probabilidade: v })}
              ariaLabel="Probabilidade"
            />
          </div>
          <div className="form-group">
            <label>Comissão Total HABITA</label>
            <NumericInputField
              kind="money"
              label="Comissão Total HABITA"
              value={form.comissaoTotalHabita}
              onChange={next => setForm({ ...form, comissaoTotalHabita: next })}
            />
          </div>
        </div>
        <div className="form-group">
          <label>Faturação (Data Prevista)</label>
          <DatePickerField
            value={form.dataPrevistaFaturacao}
            onChange={v => setForm({ ...form, dataPrevistaFaturacao: v })}
          />
        </div>
        <div className="negocio-modal-grid">
          <div className="form-group">
            <label>Ano</label>
            <input type="text" value={derived ? derived.ano : '—'} readOnly disabled aria-label="Ano (automático)" />
          </div>
          <div className="form-group">
            <label>Mês</label>
            <input type="text" value={derived ? derived.mes : '—'} readOnly disabled aria-label="Mês (automático)" />
          </div>
          <div className="form-group">
            <label>Nº Semana</label>
            <input type="text" value={derived ? derived.numeroSemana : '—'} readOnly disabled aria-label="Nº Semana (automático)" />
          </div>
        </div>
      </div>
      <div className="negocio-modal-footer">
        <button type="button" className="btn btn-secondary" onClick={onClose}>Cancelar</button>
        <button type="button" className="btn btn-primary" onClick={handleCreate} disabled={saving}>
          {saving ? 'A criar...' : 'Criar'}
        </button>
      </div>
    </div>
  );
}

function getItemFields(item) {
  if (!item) return {};
  const nested = item.fields || item.Fields;
  if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
    // Merge DTO aliases with SharePoint field bag; keep rawFields out of display lookup.
    const { fields, Fields, rawFields, RawFields, ...rest } = item;
    return { ...rest, ...nested, id: item.id || item.Id };
  }
  return item;
}

function isEmptyFieldPlaceholder(value) {
  if (value == null || value === '') return true;
  // Graph often returns multi-choice / multi-lookup as empty {}
  if (typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0) return true;
  return false;
}

function readDirectField(fields, fieldKey) {
  if (!fields || !fieldKey) return undefined;
  if (!isEmptyFieldPlaceholder(fields[fieldKey])) return fields[fieldKey];
  const pascal = fieldKey.charAt(0).toUpperCase() + fieldKey.slice(1);
  if (pascal !== fieldKey && !isEmptyFieldPlaceholder(fields[pascal])) return fields[pascal];
  const exact = Object.keys(fields).find(k => k.toLowerCase() === String(fieldKey).toLowerCase());
  if (exact && !isEmptyFieldPlaceholder(fields[exact])) return fields[exact];
  return undefined;
}

function getFieldValue(item, fieldKey) {
  const fields = getItemFields(item);
  if (!fieldKey) return null;

  const direct = readDirectField(fields, fieldKey);
  if (direct !== undefined) return direct;

  for (const alias of invFieldAliasKeys(fieldKey)) {
    const viaAlias = readDirectField(fields, alias);
    if (viaAlias !== undefined) return viaAlias;
  }

  // Fuzzy match — SharePoint internal names differ slightly across lists (encoding truncation, etc.)
  const fuzzyKey = findFuzzyFieldKey(fields, fieldKey);
  if (fuzzyKey && !isEmptyFieldPlaceholder(fields[fuzzyKey])) return fields[fuzzyKey];
  return null;
}

function findFuzzyFieldKey(fields, fieldKey) {
  const keys = Object.keys(fields).filter(k => !k.toLowerCase().endsWith('lookupid'));
  const target = String(fieldKey || '').toLowerCase();

  if (target.includes('probabil')) {
    return keys.find(k => k.toLowerCase().includes('probabil')) || null;
  }

  if (target.includes('natureza') || target.includes('naturezadeneg')) {
    return keys.find(k => k.toLowerCase().includes('natureza')) || null;
  }

  if (target.includes('refer')) {
    return keys.find(k => {
      const kl = k.toLowerCase();
      return kl.includes('refer') || kl === 'title';
    }) || null;
  }

  if (target.includes('tipo') && target.includes('cliente')) {
    return keys.find(k => {
      const kl = k.toLowerCase();
      return kl.includes('tipocliente') || kl.includes('tipodecliente')
        || (kl.includes('tipo') && kl.includes('cliente'));
    }) || null;
  }

  if (target.includes('operac') || target.includes('agenc') || target.includes('ag_x00ea') || target.includes('opera_x00e7')) {
    const targetLower = target.toLowerCase();
    // Prefer exact-ish multi-lookup Opera_x00e7__x00f5_es over singular Opera_x00e7__x00e3_o / Agência.
    const preferred = keys.find(k => {
      const kl = k.toLowerCase();
      if (kl.endsWith('lookupid')) return false;
      if (targetLower.includes('x00f5') || targetLower.includes('operacoes') || targetLower.endsWith('_es')) {
        return kl.includes('opera_x00e7') && (kl.includes('x00f5') || kl.endsWith('_es'));
      }
      return kl.includes('opera_x00e7') || kl.includes('ag_x00ea') || kl.includes('agenc') || kl === 'operacao';
    });
    if (preferred) return preferred;
    return keys.find(k => {
      const kl = k.toLowerCase();
      if (kl.endsWith('lookupid')) return false;
      return kl.includes('opera_x00e7') || kl.includes('ag_x00ea') || kl.includes('agenc') || kl === 'operacao';
    }) || null;
  }

  if (target.includes('comiss')) {
    const comissKeys = keys.filter(k => k.toLowerCase().includes('comiss'));
    if (target.includes('total') || target.includes('habita')) {
      return comissKeys.find(k => {
        const kl = k.toLowerCase();
        return kl.includes('total') || kl.includes('habita');
      }) || comissKeys[0] || null;
    }
    // Prefer plain Comissão over Comissão Total HABITA when the target is short.
    return comissKeys.find(k => !k.toLowerCase().includes('total')) || comissKeys[0] || null;
  }

  if (target.includes('montante')) {
    return keys.find(k => k.toLowerCase().includes('montante')) || null;
  }

  if (target === 'data' || target === 'created') {
    return keys.find(k => {
      const kl = k.toLowerCase();
      if (kl === 'data') return true;
      if (!kl.includes('data') && kl !== 'created') return false;
      if (kl.includes('previst') || kl.includes('efetiv') || kl.includes('modif') || kl.includes('fatur')) return false;
      return kl === 'data' || kl.startsWith('data') || kl === 'created';
    }) || null;
  }

  if (target.includes('previst') || target.includes('fatur') || target.includes('efetiv')) {
    return keys.find(k => {
      const kl = k.toLowerCase();
      return kl.includes('previst') || (kl.includes('data') && kl.includes('efetiv')) || kl.includes('fatur');
    }) || null;
  }

  // Prefix match for truncated encoded names (e.g. ProbabilidadedeConcretiza_x00e7_ vs ..._x00e3_o)
  if (target.length >= 12) {
    const prefix = target.slice(0, Math.min(24, target.length));
    return keys.find(k => k.toLowerCase().startsWith(prefix) || prefix.startsWith(k.toLowerCase().slice(0, 16))) || null;
  }

  return null;
}

/** Prefer display text for Person/Lookup columns used across Mediação / generic grids. */
function isConsultorPersonFieldKey(fieldKey) {
  const key = String(fieldKey || '').toLowerCase();
  return key.includes('consultor');
}

function formatMultiValueParts(value, displayPath = null) {
  if (!Array.isArray(value)) return null;
  const path = displayPath ? String(displayPath).trim() : '';
  const parts = value
    .map(v => {
      if (v == null || v === '') return null;
      if (typeof v === 'object') {
        if (path) {
          const viaPath = resolveDisplayValue(v, path);
          if (viaPath != null && viaPath !== '' && !isLikelyLookupIdValue(viaPath)) return String(viaPath);
        }
        const fromObj = v.lookupValue ?? v.value ?? v.Title ?? v.displayName ?? v.email ?? v.Label ?? v.label;
        if (fromObj != null && fromObj !== '' && !isLikelyLookupIdValue(fromObj)) return String(fromObj);
        return null;
      }
      const s = String(v).trim();
      return s && !isLikelyLookupIdValue(s) ? s : (s || null);
    })
    .filter(v => v != null && String(v).trim() !== '');
  return parts.length ? parts.join(', ') : null;
}

function getLookupDisplayValue(item, fieldKey, displayPath = null) {
  const raw = getFieldValue(item, fieldKey);

  // Multi-choice / multi-lookup arrays must be handled before object/special-case branches
  // (Array.isArray is true for typeof === 'object', which previously returned null).
  if (Array.isArray(raw)) {
    const joined = formatMultiValueParts(raw, displayPath);
    if (joined != null) return joined;
  }

  if (displayPath) {
    const path = String(displayPath).trim();
    if (path) {
      // Nested path on the field value itself (e.g. value / displayName).
      const resolved = resolveDisplayValue(raw, path);
      if (resolved != null && resolved !== '' && typeof resolved !== 'object') return resolved;
      const aliasPath = ['lookupvalue', 'value', 'title', 'displayname'].includes(path.toLowerCase());
      if (aliasPath && raw != null && raw !== '' && typeof raw !== 'object' && !Array.isArray(raw)) {
        return raw;
      }
      if (aliasPath && raw && typeof raw === 'object' && !Array.isArray(raw)) {
        const aliased = objectDisplayText(raw)
          ?? resolveDisplayValue(raw, 'lookupValue')
          ?? resolveDisplayValue(raw, 'value')
          ?? resolveDisplayValue(raw, 'Title')
          ?? resolveDisplayValue(raw, 'displayName');
        if (aliased != null && aliased !== '' && typeof aliased !== 'object') return aliased;
      }

      // Raw snapshot key equals the field name → show the scalar field value
      // (currency/number measures must not be treated as lookup IDs).
      if (path.toLowerCase() === String(fieldKey || '').toLowerCase()) {
        return raw;
      }

      // Sibling raw key (e.g. Ag_x00ea_nciaLookupId). Never steal Title/value from another column.
      const nestedProp = ['value', 'lookupvalue', 'title', 'displayname', 'id', 'email', 'address'].includes(path.toLowerCase());
      const fields = getItemFields(item);
      const exactSibling = nestedProp
        ? null
        : Object.keys(fields).find(k => k.toLowerCase() === path.toLowerCase());
      if (exactSibling && fields[exactSibling] != null && fields[exactSibling] !== '') {
        const sibling = fields[exactSibling];
        if (Array.isArray(sibling)) {
          const joined = formatMultiValueParts(sibling, path === 'value' ? 'value' : null);
          if (joined != null) return joined;
        }
        if (typeof sibling === 'object') {
          const fromObj = resolveDisplayValue(sibling, '') ?? objectDisplayText(sibling) ?? sibling.lookupValue ?? sibling.value ?? sibling.Title ?? sibling.displayName ?? sibling.DisplayName;
          if (fromObj != null && fromObj !== '') return fromObj;
        }
        return sibling;
      }
    }
  }

  const key = (fieldKey || '').toLowerCase();

  // Exact person field only — do NOT match valor_consultor / plano_comissional etc.
  if (isConsultorPersonFieldKey(fieldKey)) {
    const name = getFieldValue(item, 'Consultor') || getFieldValue(item, 'ConsultorNome');
    const email = getFieldValue(item, 'ConsultorEmail') || getFieldValue(item, 'consultorEmail');
    if (name && !isLikelyLookupIdValue(name)) return name;
    if (email) return email;
    return null;
  }

  if (key.includes('ag_x00ea') || key.includes('opera_x00e7') || key === 'operacao' || key === 'operação' || key.includes('agenc')) {
    const name =
      getFieldValue(item, fieldKey) ||
      getFieldValue(item, 'Ag_x00ea_ncia') ||
      getFieldValue(item, 'Opera_x00e7__x00e3_o') ||
      getFieldValue(item, 'Operacao') ||
      getFieldValue(item, 'Operação');
    if (Array.isArray(name)) {
      const joined = formatMultiValueParts(name, displayPath);
      if (joined != null) return joined;
      return null;
    }
    if (name && typeof name === 'object') {
      const fromObj = objectDisplayText(name) ?? name.lookupValue ?? name.value ?? name.Title ?? name.displayName;
      if (fromObj != null && fromObj !== '' && !isLikelyLookupIdValue(fromObj)) return fromObj;
    }
    if (name && !isLikelyLookupIdValue(name) && typeof name !== 'object') return name;
    return null;
  }

  if (key === 'entidade') {
    const name = getFieldValue(item, 'Entidade');
    if (name && !isLikelyLookupIdValue(name)) return name;
    return null;
  }

  const value = raw;
  if (Array.isArray(value)) {
    const parts = value
      .map(v => {
        if (v == null || v === '') return null;
        if (typeof v === 'object') return objectDisplayText(v) ?? v.lookupValue ?? v.value ?? v.Title ?? v.displayName ?? v.DisplayName ?? v.email ?? null;
        return String(v);
      })
      .filter(v => v != null && String(v).trim() !== '');
    return parts.length ? parts.join(', ') : null;
  }
  if (value && typeof value === 'object') {
    if (Object.keys(value).length === 0) return null;
    const fromObj = objectDisplayText(value) ?? value.lookupValue ?? value.value ?? value.Title ?? value.displayName ?? value.DisplayName ?? value.email;
    if (fromObj != null && fromObj !== '' && !isLikelyLookupIdValue(fromObj)) return fromObj;
  }
  return isLikelyLookupIdValue(value) ? null : value;
}

/** Resolve nested/object field display using admin DisplayPath (e.g. value from {id, value}). */
function resolveDisplayValue(value, displayPath) {
  if (value == null || value === '') return null;
  if (!displayPath) return value;

  const path = String(displayPath).trim();
  if (!path) return value;

  let current = value;
  if (typeof current === 'string') {
    const trimmed = current.trim();
    if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
      try { current = JSON.parse(trimmed); } catch { /* keep string */ }
    }
  }

  const parts = path.split('.').filter(Boolean);
  for (const part of parts) {
    if (current == null) return null;
    if (typeof current !== 'object') return null;
    const found = Object.keys(current).find(k => k.toLowerCase() === part.toLowerCase());
    current = found ? current[found] : undefined;
  }
  return current == null ? null : current;
}

function normalizeNbspText(value) {
  if (value == null) return value;
  if (typeof value === 'number') return value;
  return String(value).replace(/[\u00A0\u202F]/g, '').replace(/\s+/g, ' ').trim();
}

function isLikelyLookupIdValue(value) {
  if (value == null || value === '') return false;
  // Typed JSON numbers from the API are measures (comissão, montante, ano, …),
  // never unresolved SharePoint lookup IDs. Only blank bare digit *strings*.
  if (typeof value === 'number') return false;
  const s = String(value).trim();
  if (!/^\d+$/.test(s)) return false;
  const n = Number(s);
  if (n >= 1900 && n <= 2100) return false;
  if (n > 100000) return false;
  // Unresolved SharePoint lookup IDs are typically small integer strings.
  return s.length <= 5;
}

const SKIP_DISPLAY_FIELD_KEYS = new Set([
  'id', 'Id', 'ID',
  'ContentType', 'ContentTypeId', 'Attachments', 'Edit',
  'LinkTitle', 'LinkTitleNoMenu', 'ItemChildCount', 'FolderChildCount',
  'AppAuthorLookupId', 'AppEditorLookupId', '_UIVersionString', 'GUID',
  'ComplianceAssetId', 'FileSystemObjectType', 'FileLeafRef', 'FileRef',
]);

function isUsefulDisplayFieldKey(key) {
  if (!key || SKIP_DISPLAY_FIELD_KEYS.has(key)) return false;
  const kl = String(key).toLowerCase();
  if (kl.startsWith('@odata') || kl.startsWith('_')) return false;
  if (kl.endsWith('lookupid')) return false;
  if (kl === 'author' || kl === 'editor' || kl === 'created' || kl === 'modified') return false;
  return true;
}

function humanizeSpFieldKey(key) {
  return String(key)
    .replace(/_x00([0-9a-f]{2})_/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
    .replace(/_x002f_/gi, '/')
    .replace(/_x0020_/gi, ' ')
    .replace(/^(Tipo)de([A-Z])/g, '$1 de $2')
    .replace(/([a-z])([A-Z])/g, '$1 $2')
    .replace(/\s+/g, ' ')
    .trim() || key;
}

function itemMatchesEstadoFilter(item, estado, fieldKey) {
  if (!estado) return true;
  const wanted = String(estado).trim().toLowerCase();
  if (!wanted) return true;
  const keys = [];
  if (fieldKey) {
    keys.push(fieldKey);
    const alt = String(fieldKey);
    if (alt !== alt.toLowerCase()) keys.push(alt.toLowerCase());
    if (alt !== alt.toUpperCase()) keys.push(alt.charAt(0).toUpperCase() + alt.slice(1));
  }
  keys.push('validado', 'Validado', 'estado', 'Estado', 'validacao', 'Validacao');
  const seen = new Set();
  for (const key of keys) {
    const k = String(key);
    if (!k || seen.has(k.toLowerCase())) continue;
    seen.add(k.toLowerCase());
    const raw = getLookupDisplayValue(item, k) ?? getFieldValue(item, k);
    if (raw == null || raw === '') continue;
    if (String(raw).trim().toLowerCase() === wanted) return true;
  }
  return false;
}

/** If configured columns mostly miss, infer usable columns from actual SharePoint field keys. */
function inferColumnsFromItems(items, configured) {
  if (!items || items.length === 0) return configured || {};
  const configuredEntries = Object.entries(configured || {});
  const sample = items.slice(0, 8);

  let hits = 0;
  for (const [, key] of configuredEntries) {
    const found = sample.some(row => {
      const v = getLookupDisplayValue(row, key);
      return v != null && String(v).trim() !== '';
    });
    if (found) hits++;
  }
  if (configuredEntries.length > 0 && hits >= Math.max(1, Math.ceil(configuredEntries.length * 0.34))) {
    return configured;
  }

  const keyScores = new Map();
  for (const row of sample) {
    const fields = getItemFields(row);
    Object.entries(fields).forEach(([k, v]) => {
      if (!isUsefulDisplayFieldKey(k)) return;
      if (v == null || v === '') return;
      if (typeof v !== 'object' && isLikelyLookupIdValue(v)) return;
      keyScores.set(k, (keyScores.get(k) || 0) + 1);
    });
  }

  if (keyScores.size === 0) {
    const fields = getItemFields(sample[0]);
    Object.keys(fields).filter(isUsefulDisplayFieldKey).forEach(k => keyScores.set(k, 1));
  }

  const ordered = [...keyScores.entries()]
    .sort((a, b) => {
      const ap = a[0].toLowerCase() === 'title' ? 1e9 : a[1];
      const bp = b[0].toLowerCase() === 'title' ? 1e9 : b[1];
      return bp - ap;
    })
    .map(([k]) => k);

  const inferred = {};
  ordered.slice(0, 10).forEach(key => {
    inferred[humanizeSpFieldKey(key)] = key;
  });
  return Object.keys(inferred).length > 0 ? inferred : (configured || {});
}

function toFormValue(value, fieldKey) {
  if (value == null || value === '') return '';
  const key = (fieldKey || '').toLowerCase();
  if (key.includes('data') || key.endsWith('date')) {
    const d = new Date(value);
    if (!Number.isNaN(d.getTime())) return d.toISOString().slice(0, 10);
  }
  const parsed = parseMaybeJson(value);
  if (Array.isArray(parsed)) return invChoiceSet(parsed).join(', ');
  const fromObj = objectDisplayText(parsed);
  if (fromObj) return fromObj;
  if (typeof parsed === 'object' && parsed) {
    return parsed.lookupValue || parsed.displayName || parsed.DisplayName || parsed.email || JSON.stringify(parsed);
  }
  return String(parsed ?? value);
}

function fromFormValue(raw, fieldKey) {
  if (raw === '' || raw == null) return null;
  const key = (fieldKey || '').toLowerCase();
  if (key.includes('comissao') || key.includes('montante') || key.includes('valor')) {
    const n = parseFloat(raw);
    return Number.isNaN(n) ? raw : n;
  }
  if (key.includes('data') || key.endsWith('date')) {
    return raw ? new Date(raw).toISOString() : null;
  }
  if (key === 'ano' || key === 'mes' || key === 'm_x00ea_s' || key.includes('semana')) {
    const n = parseInt(String(raw), 10);
    return Number.isNaN(n) ? raw : n;
  }
  return raw;
}

/** Convert { Label: key } maps or settings GridColumns arrays into normalized column defs. */
function toColumnDefs(configured) {
  if (!configured) return [];
  if (Array.isArray(configured)) {
    return configured
      .map((c, i) => ({
        key: c.key || c.Key,
        label: c.label || c.Label || humanizeSpFieldKey(c.key || c.Key || ''),
        visible: c.visible !== false && c.Visible !== false,
        order: c.order ?? c.Order ?? i,
        displayPath: c.displayPath ?? c.DisplayPath ?? null,
        format: c.format ?? c.Format ?? null,
        aliasKey: c.aliasKey ?? c.AliasKey ?? null,
        width: c.width ?? c.Width ?? null,
        type: c.type ?? c.Type ?? null,
        filterType: c.filterType ?? c.FilterType ?? null,
        choices: c.choices || c.Choices || null,
        filterable: c.filterable !== false && c.Filterable !== false,
        sortable: c.sortable !== false && c.Sortable !== false,
        fill: c.fill === true || c.Fill === true || c.flex === true || c.Flex === true,
        fixed: c.fixed === true || c.Fixed === true,
        columnKind: c.columnKind || c.ColumnKind || c.type || c.Type || null,
        allowMultiple: c.allowMultiple === true || c.AllowMultiple === true,
        system: c.system === true || c.isSystem === true || c._isSystem === true || c.IsSystem === true,
      }))
      .filter(c => c.key)
      .sort((a, b) => a.order - b.order);
  }
  return Object.entries(configured).map(([label, key], i) => ({
    key,
    label,
    visible: true,
    order: i,
    displayPath: null,
    format: null,
    width: null,
    type: null,
    filterType: null,
    choices: null,
    filterable: true,
    sortable: true,
  }));
}

const GRID_FILTER_OPS = [
  { value: 'contains', label: 'contém' },
  { value: 'equals', label: 'é igual a' },
  { value: 'startsWith', label: 'começa com' },
  { value: 'endsWith', label: 'termina com' },
  { value: 'notContains', label: 'não contém' },
  { value: 'isEmpty', label: 'está vazio' },
  { value: 'notEmpty', label: 'não está vazio' },
];

function emptyColumnFilter() {
  return { op1: 'contains', val1: '', logic: 'and', op2: 'contains', val2: '' };
}

function isChoiceGridColumn(col) {
  if (!col) return false;
  if (Array.isArray(col.choices) && col.choices.length > 0) return true;
  const t = String(col.filterType || col.type || '').toLowerCase();
  if (t === 'choice' || t.includes('choice') || t === 'boolean' || t === 'bool') return true;
  const kl = String(col.key || '').toLowerCase();
  return ['estado', 'validacao', 'validado', 'origem', 'natureza', 'naturezanegocio', 'probabilidade', 'enabled'].includes(kl);
}

function formatChoiceOptionLabel(value) {
  if (value === true || value === 'true') return 'Sim / Ativa';
  if (value === false || value === 'false') return 'Não / Inativa';
  return String(value);
}

function getColumnChoiceOptions(col, rows) {
  if (Array.isArray(col.choices) && col.choices.length > 0) {
    return col.choices.map(c => (typeof c === 'object' ? c : { value: String(c), label: formatChoiceOptionLabel(c) }));
  }
  const seen = new Set();
  const opts = [];
  (rows || []).forEach(row => {
    const raw = getGridCellRaw(row, col);
    if (raw == null || raw === '') return;
    const value = typeof raw === 'boolean' ? String(raw) : String(raw);
    if (seen.has(value)) return;
    seen.add(value);
    opts.push({ value, label: formatChoiceOptionLabel(raw) });
  });
  return opts.sort((a, b) => a.label.localeCompare(b.label, 'pt', { sensitivity: 'base' }));
}

function matchGridFilterOp(raw, op, filterVal) {
  const s = raw == null ? '' : String(raw).toLowerCase();
  const f = filterVal == null ? '' : String(filterVal).toLowerCase();
  switch (op) {
    case 'equals': return s === f;
    case 'startsWith': return s.startsWith(f);
    case 'endsWith': return s.endsWith(f);
    case 'notContains': return !s.includes(f);
    case 'isEmpty': return s.trim() === '';
    case 'notEmpty': return s.trim() !== '';
    case 'contains':
    default:
      if (!f) return true;
      return s.includes(f);
  }
}

function columnFilterIsActive(f) {
  if (!f) return false;
  if (f.op1 === 'isEmpty' || f.op1 === 'notEmpty') return true;
  if (String(f.val1 || '').trim()) return true;
  if (f.op2 === 'isEmpty' || f.op2 === 'notEmpty') return true;
  if (String(f.val2 || '').trim()) return true;
  return false;
}

/** Sum money-like fields on the current page rows for list footers. */
function sumListMoneyTotals(rows, columnDefs) {
  const keys = (columnDefs || []).map(c => String(c.key || ''));
  const findKey = (pred) => keys.find(k => pred(k.toLowerCase())) || null;
  const montanteKey = findKey(k => k.includes('montante') || k === 'valor' || k.endsWith('_valor'));
  const comissaoKey = findKey(k =>
    (k.includes('comiss') || k.includes('comissao'))
    && !k.includes('consultor')
    && !k.includes('valor_consultor'));
  const valorKey = !montanteKey ? findKey(k => {
    if (k.includes('min') || k.includes('max')) return false;
    return k === 'valor' || k.includes('valor');
  }) : null;

  const parseMoney = (row, key) => {
    if (!key) return 0;
    const raw = getFieldValue(row, key);
    if (raw == null || raw === '') return 0;
    if (typeof raw === 'number') return Number.isFinite(raw) ? raw : 0;
    const n = parseFloat(String(raw).replace(/\s/g, '').replace(',', '.'));
    return Number.isFinite(n) ? n : 0;
  };

  let sumMontante = 0;
  let sumComissao = 0;
  let sumValor = 0;
  (rows || []).forEach(row => {
    if (montanteKey) sumMontante += parseMoney(row, montanteKey);
    if (comissaoKey) sumComissao += parseMoney(row, comissaoKey);
    if (valorKey) sumValor += parseMoney(row, valorKey);
  });

  return {
    count: (rows || []).length,
    sumMontante: montanteKey ? sumMontante : null,
    sumComissao: comissaoKey ? sumComissao : null,
    sumValor: valorKey ? sumValor : null,
  };
}

function ListTotalsCards({ rows, columnDefs, extraCards = [], includeMoney = true }) {
  const totals = useMemo(
    () => sumListMoneyTotals(rows, toColumnDefs(columnDefs)),
    [rows, columnDefs]
  );
  const cards = [...(extraCards || [])];
  if (includeMoney) {
    if (totals.sumMontante != null) {
      cards.push({ key: 'montante', icon: 'graph', title: 'Montante', value: formatCurrency(totals.sumMontante) });
    }
    if (totals.sumComissao != null) {
      cards.push({ key: 'comissao', icon: 'trophy', title: 'Comissão', value: formatCurrency(totals.sumComissao) });
    }
    if (totals.sumValor != null) {
      cards.push({ key: 'valor', icon: 'graph', title: 'Valor', value: formatCurrency(totals.sumValor) });
    }
  }
  if (!cards.length) return null;
  return (
    <div className={`list-totals-cards n-${cards.length}`}>
      {cards.map(card => {
        const node = (
          <DashCard
            icon={card.icon}
            title={card.title}
            className={`list-totals-card${card.glow ? ' is-glow' : ''}${card.active ? ' is-active' : ''}${card.onClick ? ' is-clickable' : ''}`}
            disableCompact
            onClick={card.onClick}
            role={card.onClick ? 'button' : undefined}
            tabIndex={card.onClick ? 0 : undefined}
          >
            <div className="kpi-card-v2-value">{card.value}</div>
          </DashCard>
        );
        return card.tip ? (
          <AppTooltip key={card.key} label={card.tip}>{node}</AppTooltip>
        ) : (
          <React.Fragment key={card.key}>{node}</React.Fragment>
        );
      })}
    </div>
  );
}

function listHasMoneyTotals(rows, columnDefs) {
  const totals = sumListMoneyTotals(rows, toColumnDefs(columnDefs));
  return totals.sumMontante != null || totals.sumComissao != null || totals.sumValor != null;
}

function rowMatchesColumnFilter(row, col, f) {
  if (!columnFilterIsActive(f)) return true;
  const raw = getGridCellRaw(row, col);
  const m1 = matchGridFilterOp(raw, f.op1 || 'contains', f.val1);
  const secondActive = f.op2 === 'isEmpty' || f.op2 === 'notEmpty' || String(f.val2 || '').trim();
  if (!secondActive) return m1;
  const m2 = matchGridFilterOp(raw, f.op2 || 'contains', f.val2);
  return (f.logic || 'and') === 'or' ? (m1 || m2) : (m1 && m2);
}

function columnDefsToMap(defs) {
  const map = {};
  (defs || []).filter(c => c.visible !== false).forEach(c => { map[c.label] = c.key; });
  return map;
}

function loadGridLayout(storageKey) {
  try {
    const raw = window.localStorage.getItem(`myhabita.grid.${storageKey}`);
    if (!raw) return null;
    const parsed = JSON.parse(raw);
    // v3+: auto-fit by cell content (not header label length).
    if (!parsed || parsed.v !== 3) {
      return {
        ...(parsed || {}),
        v: 3,
        widths: {},
      };
    }
    return parsed;
  } catch {
    return null;
  }
}

function saveGridLayout(storageKey, layout) {
  try {
    window.localStorage.setItem(
      `myhabita.grid.${storageKey}`,
      JSON.stringify({ ...(layout || {}), v: 3 })
    );
  } catch { /* ignore */ }
}

function gridViewStateKey(storageKey) {
  return `myhabita.grid.view.${storageKey}`;
}

function loadGridViewState(storageKey) {
  try {
    const raw = window.localStorage.getItem(gridViewStateKey(storageKey));
    if (!raw) return null;
    const parsed = JSON.parse(raw);
    if (!parsed || typeof parsed !== 'object') return null;
    return {
      sort: parsed.sort?.key
        ? { key: parsed.sort.key, dir: parsed.sort.dir === 'desc' ? 'desc' : 'asc' }
        : { key: null, dir: 'asc' },
      filters: parsed.filters && typeof parsed.filters === 'object' ? parsed.filters : {},
      globalQuery: typeof parsed.globalQuery === 'string' ? parsed.globalQuery : '',
    };
  } catch {
    return null;
  }
}

function saveGridViewState(storageKey, state) {
  try {
    window.localStorage.setItem(
      gridViewStateKey(storageKey),
      JSON.stringify({
        sort: state?.sort || { key: null, dir: 'asc' },
        filters: state?.filters || {},
        globalQuery: state?.globalQuery || '',
      })
    );
  } catch { /* ignore */ }
}

function clearGridViewState(storageKey) {
  try {
    window.localStorage.removeItem(gridViewStateKey(storageKey));
  } catch { /* ignore */ }
}

function mergeColumnLayout(baseDefs, layout, preferSourceOrder = false) {
  const defs = toColumnDefs(baseDefs);
  if (!layout) return defs;

  const byKey = Object.fromEntries(defs.map(d => [d.key, { ...d }]));
  const ordered = [];

  const resolveVisible = (key, baseVisible) => {
    if (layout.visibility && Object.prototype.hasOwnProperty.call(layout.visibility, key)) {
      return !!layout.visibility[key];
    }
    return baseVisible !== false;
  };

  const applyWidths = (col) => ({
    ...col,
    width: col.fixed ? col.width : (layout.widths?.[col.key] ?? col.width),
    visible: resolveVisible(col.key, col.visible),
  });

  // Settings / configured column order wins (ignore stale localStorage order).
  if (preferSourceOrder) {
    return defs
      .map((d, i) => applyWidths({ ...d, order: d.order ?? i }))
      .sort((a, b) => a.order - b.order);
  }

  (layout.order || []).forEach((key, i) => {
    if (!byKey[key]) return;
    ordered.push({
      ...byKey[key],
      order: i,
      visible: resolveVisible(key, byKey[key].visible),
      width: byKey[key].fixed ? byKey[key].width : (layout.widths?.[key] ?? byKey[key].width),
    });
    delete byKey[key];
  });

  Object.values(byKey).forEach((d, i) => {
    ordered.push({
      ...d,
      order: ordered.length + i,
      visible: resolveVisible(d.key, d.visible),
      width: d.fixed ? d.width : (layout.widths?.[d.key] ?? d.width),
    });
  });

  return ordered.sort((a, b) => a.order - b.order);
}

function getGridCellRaw(row, col) {
  const primary = getLookupDisplayValue(row, col.key, col.displayPath);
  if (primary != null && primary !== '') return primary;
  if (col.aliasKey && col.aliasKey !== col.key) {
    const viaAlias = getLookupDisplayValue(row, col.aliasKey, col.displayPath);
    if (viaAlias != null && viaAlias !== '') return viaAlias;
  }
  // Comissões / pedidos: visible "Ano" is often empty; year lives in Ano de Criação.
  const key = String(col.key || '').toLowerCase();
  if (key === 'ano') {
    const createdYear = getLookupDisplayValue(row, 'Ano_x0020_de_x0020_Cria_x00e7__x', col.displayPath)
      ?? getLookupDisplayValue(row, 'Ano_x0020_de_x0020_Criacao', col.displayPath);
    if (createdYear != null && createdYear !== '') return createdYear;
  }
  return primary;
}

const GRID_AUTO_WIDTH = {
  min: 72,
  max: 360,
  /** Room for sort/filter/resize chrome only — not full header text. */
  chromePad: 44,
  cellPad: 28,
  sampleRows: 80,
};

let _gridMeasureCtx = null;
function measureGridTextPx(text, font) {
  if (typeof document === 'undefined') {
    return Math.max(0, String(text ?? '').length * 7.2);
  }
  if (!_gridMeasureCtx) {
    _gridMeasureCtx = document.createElement('canvas').getContext('2d');
  }
  if (!_gridMeasureCtx) return Math.max(0, String(text ?? '').length * 7.2);
  _gridMeasureCtx.font = font;
  return _gridMeasureCtx.measureText(String(text ?? '')).width;
}

/** Auto width from cell content only; long headers ellipsize instead of stretching columns. */
function estimateGridColumnWidth(col, rows) {
  if (col?.fixed && col.width != null) return Number(col.width);
  const cellFont = '400 12px Roboto, system-ui, sans-serif';
  let maxPx = GRID_AUTO_WIDTH.min;

  const sample = (rows || []).slice(0, GRID_AUTO_WIDTH.sampleRows);
  let sawContent = false;
  for (let i = 0; i < sample.length; i += 1) {
    const raw = getGridCellRaw(sample[i], col);
    if (raw == null || raw === '') continue;
    const display = formatCellValue(raw, col.key, col.format);
    const text = display == null || display === '—' ? '' : String(display);
    if (!text) continue;
    sawContent = true;
    let w = measureGridTextPx(text, cellFont) + GRID_AUTO_WIDTH.cellPad;
    const kl = String(col.key || '').toLowerCase();
    if (kl === 'estado' || kl === 'validacao' || kl === 'validado' || kl === 'origem') {
      w += 12; // status badge padding
    }
    if (w > maxPx) maxPx = w;
  }

  // Empty column: keep compact; only reserve chrome for filter/sort controls.
  if (!sawContent) {
    maxPx = Math.max(GRID_AUTO_WIDTH.min, GRID_AUTO_WIDTH.chromePad + 24);
  }

  return Math.round(Math.min(GRID_AUTO_WIDTH.max, Math.max(GRID_AUTO_WIDTH.min, maxPx)));
}

function buildGridAutoWidths(columns, rows) {
  const widths = {};
  (columns || []).forEach((col) => {
    if (!col?.key) return;
    widths[col.key] = estimateGridColumnWidth(col, rows);
  });
  return widths;
}

/** Column that absorbs leftover horizontal space so the table fills the viewport. */
function isDataGridFillColumn(col) {
  if (!col) return false;
  if (col.fill === true || col.flex === true) return true;
  const keys = [col.key, col.aliasKey].map((x) => String(x || '').toLowerCase());
  const label = String(col.label || '').toLowerCase();
  return keys.includes('consultor')
    || keys.includes('consultornome')
    || keys.includes('title')
    || keys.includes('ativo')
    || keys.includes('assinatura')
    || label === 'consultor'
    || label === 'ativo'
    || label === 'oportunidade'
    || label === 'cliente';
}

function DataGrid({
  storageKey,
  columns,
  rows,
  selectedId = null,
  onRowClick = null,
  onRowDoubleClick = null,
  renderActions = null,
  renderCell = null,
  emptyMessage = 'Sem registos.',
  showColumnChooser = true,
  /** 'client' filters/sorts current page; 'server' notifies parent and never mutates local rows. */
  filterMode = 'client',
  onColumnFiltersChange = null,
  onSortChange = null,
  onGlobalQueryChange = null,
  /** When set, rows can be dragged to reorder. Called with (fromRowId, toRowId). */
  onRowReorder = null,
  /** Prefer column order from `columns` prop over localStorage layout order. */
  preferSourceOrder = false,
}) {
  const baseDefs = useMemo(() => toColumnDefs(columns), [columns]);
  const [layout, setLayout] = useState(() => loadGridLayout(storageKey) || {});
  const [sort, setSort] = useState(() => loadGridViewState(storageKey)?.sort || { key: null, dir: 'asc' });
  const [filters, setFilters] = useState(() => loadGridViewState(storageKey)?.filters || {});
  const [globalQuery, setGlobalQuery] = useState(() => loadGridViewState(storageKey)?.globalQuery || '');
  const [chooserOpen, setChooserOpen] = useState(false);
  const [menu, setMenu] = useState(null); // { key, draft }
  const [rowDragId, setRowDragId] = useState(null);
  const [rowDragOverId, setRowDragOverId] = useState(null);
  const dragKey = useRef(null);
  const resizeState = useRef(null);
  const menuRef = useRef(null);
  const scrollRef = useRef(null);
  const [containerWidth, setContainerWidth] = useState(0);
  const onFiltersChangeRef = useRef(onColumnFiltersChange);
  onFiltersChangeRef.current = onColumnFiltersChange;
  const onSortChangeRef = useRef(onSortChange);
  onSortChangeRef.current = onSortChange;
  const onGlobalQueryChangeRef = useRef(onGlobalQueryChange);
  onGlobalQueryChangeRef.current = onGlobalQueryChange;
  const rowReorderEnabled = typeof onRowReorder === 'function';
  const isServerData = filterMode === 'server';

  const defs = useMemo(
    () => mergeColumnLayout(baseDefs, layout, preferSourceOrder),
    [baseDefs, layout, preferSourceOrder]
  );

  useEffect(() => {
    const view = loadGridViewState(storageKey);
    setLayout(loadGridLayout(storageKey) || {});
    setSort(view?.sort || { key: null, dir: 'asc' });
    setFilters(view?.filters || {});
    setGlobalQuery(view?.globalQuery || '');
    setMenu(null);
  }, [storageKey]);

  useEffect(() => {
    const el = scrollRef.current;
    if (!el) return undefined;
    const update = () => setContainerWidth(Math.floor(el.clientWidth));
    update();
    if (typeof ResizeObserver === 'undefined') {
      window.addEventListener('resize', update);
      return () => window.removeEventListener('resize', update);
    }
    const ro = new ResizeObserver(() => update());
    ro.observe(el);
    return () => ro.disconnect();
  }, [storageKey]);

  useEffect(() => {
    onFiltersChangeRef.current?.(filters);
  }, [filters]);

  useEffect(() => {
    onSortChangeRef.current?.(sort);
  }, [sort]);

  useEffect(() => {
    if (!isServerData) return undefined;
    const timer = setTimeout(() => {
      onGlobalQueryChangeRef.current?.(globalQuery);
    }, 350);
    return () => clearTimeout(timer);
  }, [globalQuery, isServerData]);

  useEffect(() => {
    if (!storageKey) return;
    saveGridViewState(storageKey, { sort, filters, globalQuery });
  }, [storageKey, sort, filters, globalQuery]);
  useEffect(() => {
    if (!menu) return undefined;
    const onDoc = (e) => {
      if (menuRef.current && !menuRef.current.contains(e.target)
          && !e.target.closest?.('.data-grid-th-menu-btn')
          && !e.target.closest?.('.app-select-popover')) {
        setMenu(null);
      }
    };
    const onKey = (e) => { if (e.key === 'Escape') setMenu(null); };
    const onRepositionClose = (e) => {
      if (e?.target && (menuRef.current?.contains(e.target) || e.target.closest?.('.app-select-popover'))) return;
      setMenu(null);
    };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onKey);
    window.addEventListener('resize', onRepositionClose);
    window.addEventListener('scroll', onRepositionClose, true);
    return () => {
      document.removeEventListener('mousedown', onDoc);
      document.removeEventListener('keydown', onKey);
      window.removeEventListener('resize', onRepositionClose);
      window.removeEventListener('scroll', onRepositionClose, true);
    };
  }, [menu]);

  const persist = (next) => {
    setLayout(next);
    saveGridLayout(storageKey, next);
  };

  const visibleDefs = defs.filter(d => d.visible !== false);

  const autoWidths = useMemo(
    () => buildGridAutoWidths(visibleDefs, rows),
    // visibleDefs identity changes often; key off column keys + row sample fingerprint
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [storageKey, rows, visibleDefs.map(d => d.key).join('|'), visibleDefs.map(d => d.label).join('|')]
  );

  const fillColKey = useMemo(() => {
    const col = visibleDefs.find(isDataGridFillColumn);
    return col?.key ?? null;
  }, [visibleDefs]);

  const resolveBaseColWidth = (col) => {
    if (col.fixed && col.width != null) return Number(col.width);
    if (layout.widths && Object.prototype.hasOwnProperty.call(layout.widths, col.key)
        && layout.widths[col.key] != null) {
      return Number(layout.widths[col.key]);
    }
    if (col.width != null) return Number(col.width);
    if (autoWidths[col.key] != null) return Number(autoWidths[col.key]);
    return GRID_AUTO_WIDTH.min;
  };

  const resolveColWidth = (col) => {
    const base = resolveBaseColWidth(col);
    if (!fillColKey || col.key !== fillColKey || !(containerWidth > 0)) return base;
    let others = renderActions ? 100 : 0;
    if (rowReorderEnabled) others += 52;
    visibleDefs.forEach((c) => {
      if (c.key === fillColKey) return;
      others += resolveBaseColWidth(c);
    });
    return Math.max(base, Math.floor(containerWidth - others));
  };

  const tableAutoWidth = useMemo(() => {
    let total = renderActions ? 100 : 0;
    if (rowReorderEnabled) total += 52;
    visibleDefs.forEach((col) => {
      total += resolveColWidth(col);
    });
    return Math.max(total, containerWidth || 1, 1);
  }, [visibleDefs, layout.widths, autoWidths, renderActions, rowReorderEnabled, containerWidth, fillColKey]);

  const colSizeStyle = (col) => {
    const w = resolveColWidth(col);
    const isFill = fillColKey && col.key === fillColKey;
    if (isFill) {
      const minW = resolveBaseColWidth(col);
      return { width: w, minWidth: minW };
    }
    return { width: w, minWidth: w, maxWidth: w };
  };
  const openColumnMenu = (e, col) => {
    e.preventDefault();
    e.stopPropagation();
    setChooserOpen(false);
    const rect = e.currentTarget.getBoundingClientRect();
    const menuWidth = 260;
    const menuMaxH = Math.min(420, window.innerHeight - 16);
    const left = Math.max(8, Math.min(rect.left, window.innerWidth - menuWidth - 8));
    let top = rect.bottom + 4;
    if (top + 280 > window.innerHeight) top = Math.max(8, rect.top - menuMaxH - 4);
    if (top + menuMaxH > window.innerHeight) top = Math.max(8, window.innerHeight - menuMaxH - 8);
    setMenu(prev => {
      if (prev?.key === col.key) return null;
      const existing = filters[col.key];
      const choice = isChoiceGridColumn(col);
      const draft = existing
        ? { ...emptyColumnFilter(), ...existing }
        : {
            ...emptyColumnFilter(),
            op1: choice ? 'equals' : 'contains',
            op2: choice ? 'equals' : 'contains',
          };
      return { key: col.key, draft, anchor: { top, left } };
    });
  };

  const applyColumnFilter = () => {
    if (!menu) return;
    const draft = menu.draft || emptyColumnFilter();
    setFilters(prev => {
      const next = { ...prev };
      if (columnFilterIsActive(draft)) next[menu.key] = { ...draft };
      else delete next[menu.key];
      return next;
    });
    setMenu(null);
  };

  const clearColumnFilter = () => {
    if (!menu) return;
    setFilters(prev => {
      const next = { ...prev };
      delete next[menu.key];
      return next;
    });
    setMenu(null);
  };

  const setSortDir = (key, dir) => {
    setSort({ key, dir });
    setMenu(null);
  };

  const clearSort = () => {
    setSort({ key: null, dir: 'asc' });
    setMenu(null);
  };

  const toggleVisible = (key) => {
    const visibility = { ...(layout.visibility || {}) };
    const current = defs.find(d => d.key === key);
    const nowVisible = !(current?.visible !== false);
    visibility[key] = nowVisible;
    const order = layout.order || defs.map(d => d.key);
    persist({ ...layout, visibility, order });
  };

  const onDragStart = (key) => { dragKey.current = key; };
  const onDragOver = (e) => { e.preventDefault(); };
  const onDrop = (targetKey) => {
    const from = dragKey.current;
    dragKey.current = null;
    if (!from || from === targetKey) return;
    const order = (layout.order || defs.map(d => d.key)).slice();
    const fi = order.indexOf(from);
    const ti = order.indexOf(targetKey);
    if (fi < 0 || ti < 0) {
      const keys = defs.map(d => d.key);
      const f2 = keys.indexOf(from);
      const t2 = keys.indexOf(targetKey);
      if (f2 < 0 || t2 < 0) return;
      keys.splice(f2, 1);
      keys.splice(t2, 0, from);
      persist({ ...layout, order: keys });
      return;
    }
    order.splice(fi, 1);
    order.splice(ti, 0, from);
    persist({ ...layout, order });
  };

  const onResizeStart = (e, key) => {
    e.preventDefault();
    e.stopPropagation();
    const startX = e.clientX;
    const startW = resolveColWidth(defs.find(d => d.key === key) || { key })
      || e.currentTarget.parentElement?.offsetWidth
      || GRID_AUTO_WIDTH.min;
    resizeState.current = { key, startX, startW };
    const onMove = (ev) => {
      if (!resizeState.current) return;
      const { key: k, startX: sx, startW: sw } = resizeState.current;
      const width = Math.max(GRID_AUTO_WIDTH.min, sw + (ev.clientX - sx));
      const widths = { ...(layout.widths || {}), [k]: width };
      setLayout(prev => ({ ...prev, widths }));
    };
    const onUp = () => {
      if (resizeState.current) {
        const widths = { ...(layout.widths || {}) };
        const k = resizeState.current.key;
        const el = document.querySelector(`th[data-col-key="${CSS.escape(k)}"]`);
        if (el) widths[k] = el.offsetWidth;
        persist({ ...layout, widths });
      }
      resizeState.current = null;
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
    };
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
  };

  const onResizeReset = (e, key) => {
    e.preventDefault();
    e.stopPropagation();
    const widths = { ...(layout.widths || {}) };
    delete widths[key];
    persist({ ...layout, widths });
  };

  const filteredSorted = useMemo(() => {
    let list = [...(rows || [])];
    if (isServerData) {
      // Server owns search/sort/paging. Still apply column filters locally so the
      // menu is never a no-op (and as a safety net if the API has not restarted yet).
      visibleDefs.forEach(col => {
        if (col.filterable === false) return;
        list = list.filter(row => rowMatchesColumnFilter(row, col, filters[col.key]));
      });
      return list;
    }
    const gq = globalQuery.trim().toLowerCase();
    if (gq) {
      list = list.filter(row => {
        const consultorCols = visibleDefs.filter(col => {
          const k = String(col.key || col.aliasKey || '').toLowerCase();
          return k.includes('consultor');
        });
        const cols = consultorCols.length ? consultorCols : visibleDefs.filter(col => {
          const k = String(col.key || col.aliasKey || '').toLowerCase();
          return k === 'consultor' || k === 'consultornome' || k === 'consultoremail';
        });
        if (!cols.length) return false;
        return cols.some(col => String(getGridCellRaw(row, col) ?? '').toLowerCase().includes(gq));
      });
    }
    visibleDefs.forEach(col => {
      if (col.filterable === false) return;
      list = list.filter(row => rowMatchesColumnFilter(row, col, filters[col.key]));
    });
    if (sort.key && !rowReorderEnabled) {
      const col = defs.find(d => d.key === sort.key);
      list.sort((a, b) => {
        const av = getGridCellRaw(a, col || { key: sort.key });
        const bv = getGridCellRaw(b, col || { key: sort.key });
        const an = typeof av === 'number' ? av : parseFloat(av);
        const bn = typeof bv === 'number' ? bv : parseFloat(bv);
        let cmp = 0;
        if (!Number.isNaN(an) && !Number.isNaN(bn)) cmp = an - bn;
        else cmp = String(av ?? '').localeCompare(String(bv ?? ''), 'pt', { sensitivity: 'base' });
        return sort.dir === 'desc' ? -cmp : cmp;
      });
    }
    return list;
  }, [rows, visibleDefs, filters, sort, defs, globalQuery, isServerData, rowReorderEnabled]);

  const colSpanBase = visibleDefs.length + (renderActions ? 1 : 0) + (rowReorderEnabled ? 1 : 0);

  const handleRowDragStart = (e, rowId) => {
    if (!rowReorderEnabled) return;
    setRowDragId(String(rowId));
    e.dataTransfer.effectAllowed = 'move';
    try { e.dataTransfer.setData('text/plain', String(rowId)); } catch { /* ie */ }
  };

  const handleRowDragOver = (e, rowId) => {
    if (!rowReorderEnabled || rowDragId == null) return;
    e.preventDefault();
    e.dataTransfer.dropEffect = 'move';
    const id = String(rowId);
    if (id !== rowDragOverId) setRowDragOverId(id);
  };

  const handleRowDrop = (e, rowId) => {
    if (!rowReorderEnabled) return;
    e.preventDefault();
    const fromId = rowDragId || e.dataTransfer.getData('text/plain');
    const toId = String(rowId);
    setRowDragId(null);
    setRowDragOverId(null);
    if (!fromId || fromId === toId) return;
    onRowReorder(fromId, toId);
  };

  const handleRowDragEnd = () => {
    setRowDragId(null);
    setRowDragOverId(null);
  };

  const menuCol = menu ? defs.find(d => d.key === menu.key) : null;
  const menuIsChoice = menuCol ? isChoiceGridColumn(menuCol) : false;
  const menuChoices = menuCol && menuIsChoice ? getColumnChoiceOptions(menuCol, rows) : [];
  const needsValue1 = menu?.draft && menu.draft.op1 !== 'isEmpty' && menu.draft.op1 !== 'notEmpty';
  const needsValue2 = menu?.draft && menu.draft.op2 !== 'isEmpty' && menu.draft.op2 !== 'notEmpty';

  const renderFilterValue = (which) => {
    const draft = menu?.draft || emptyColumnFilter();
    const valKey = which === 1 ? 'val1' : 'val2';
    const value = draft[valKey] || '';
    const onChange = (v) => setMenu(m => m ? { ...m, draft: { ...m.draft, [valKey]: v } } : m);
    if (menuIsChoice) {
      return (
        <AppSelect
          layout="list"
          variant="compact"
          showMenuTitle={false}
          value={value}
          emptyOption="(qualquer)"
          options={menuChoices}
          onChange={onChange}
          ariaLabel="Valor do filtro"
        />
      );
    }
    return (
      <input
        className="data-grid-menu-input"
        type="text"
        value={value}
        placeholder="Valor…"
        onChange={e => onChange(e.target.value)}
      />
    );
  };

  return (
    <div className="data-grid">
      <div className="data-table-wrap data-grid-scroll" ref={scrollRef}>
        <table className="data-table data-grid-table data-grid-table-autofit" style={{ width: tableAutoWidth, minWidth: '100%' }}>
          <colgroup>
            {rowReorderEnabled && <col style={{ width: 52 }} />}
            {visibleDefs.map(col => (
              <col key={col.key} style={colSizeStyle(col)} />
            ))}
            {renderActions && <col style={{ width: 100 }} />}
          </colgroup>
          <thead>
            <tr>
              {rowReorderEnabled && (
                <th className="data-grid-row-drag-th" title="Arrastar para reordenar" aria-label="Ordem">
                  <Icon name="bars" size={12} />
                </th>
              )}
              {visibleDefs.map(col => {
                const activeFilter = columnFilterIsActive(filters[col.key]);
                const sortedHere = !rowReorderEnabled && sort.key === col.key;
                return (
                  <th
                    key={col.key}
                    data-col-key={col.key}
                    style={colSizeStyle(col)}
                    draggable={!rowReorderEnabled}
                    onDragStart={rowReorderEnabled ? undefined : () => onDragStart(col.key)}
                    onDragOver={rowReorderEnabled ? undefined : onDragOver}
                    onDrop={rowReorderEnabled ? undefined : () => onDrop(col.key)}
                    className={`data-grid-th${activeFilter ? ' is-filtered' : ''}${sortedHere ? ' is-sorted' : ''}`}
                  >
                    <div className="data-grid-th-inner">
                      <span className="data-grid-th-label" title={col.label}>
                        {col.label}
                        {sortedHere ? (sort.dir === 'asc' ? ' ▲' : ' ▼') : ''}
                      </span>
                      <span className="data-grid-th-actions">
                        {activeFilter && <Icon name="filter" size={11} />}
                        <button
                          type="button"
                          className="data-grid-th-menu-btn"
                          title="Ordenar e filtrar"
                          onMouseDown={(e) => e.stopPropagation()}
                          onClick={(e) => openColumnMenu(e, col)}
                        >
                          <Icon name="bars" size={12} />
                        </button>
                      </span>
                    </div>
                    {col.fixed ? null : (
                    <span
                      className="data-grid-resize"
                      onMouseDown={(e) => onResizeStart(e, col.key)}
                      onDoubleClick={(e) => onResizeReset(e, col.key)}
                      title="Redimensionar (duplo clique: automático)"
                    />
                    )}
                  </th>
                );
              })}
              {renderActions && <th className="col-acoes">Ações</th>}
            </tr>
          </thead>
          <tbody>
            {filteredSorted.length === 0 ? (
              <tr>
                <td colSpan={colSpanBase} style={{ textAlign: 'center', color: '#64748b' }}>
                  {emptyMessage}
                </td>
              </tr>
            ) : filteredSorted.map((row, rowIndex) => {
              const rowId = row.id || row.Id;
              const idStr = String(rowId);
              const isDragging = rowReorderEnabled && rowDragId === idStr;
              const isDragOver = rowReorderEnabled && rowDragOverId === idStr && rowDragId !== idStr;
              return (
                <tr
                  key={rowId}
                  className={[
                    selectedId != null && String(selectedId) === idStr ? 'selected' : '',
                    isDragging ? 'row-dragging' : '',
                    isDragOver ? 'row-drag-over' : '',
                  ].filter(Boolean).join(' ')}
                  onClick={onRowClick ? () => onRowClick(row) : undefined}
                  onDoubleClick={onRowDoubleClick ? () => onRowDoubleClick(row) : undefined}
                  style={(onRowClick || onRowDoubleClick) ? { cursor: 'pointer' } : undefined}
                  onDragOver={rowReorderEnabled ? (e) => handleRowDragOver(e, rowId) : undefined}
                  onDrop={rowReorderEnabled ? (e) => handleRowDrop(e, rowId) : undefined}
                >
                  {rowReorderEnabled && (
                    <td className="data-grid-row-drag-td">
                      <button
                        type="button"
                        className="data-grid-row-drag-handle"
                        title={`Arrastar para reordenar (${rowIndex + 1})`}
                        draggable
                        onDragStart={(e) => handleRowDragStart(e, rowId)}
                        onDragEnd={handleRowDragEnd}
                        onClick={(e) => e.stopPropagation()}
                      >
                        <Icon name="bars" size={14} />
                        <span className="data-grid-row-drag-index">{rowIndex}</span>
                      </button>
                    </td>
                  )}
                  {visibleDefs.map(col => {
                    if (typeof renderCell === 'function') {
                      const custom = renderCell(row, col);
                      if (custom !== undefined) {
                        return (
                          <td
                            key={col.key}
                            data-col-key={col.key}
                            className={col.fixed ? 'data-grid-td-fixed' : undefined}
                          >
                            {custom}
                          </td>
                        );
                      }
                    }
                    const value = getGridCellRaw(row, col);
                    const kl = col.key.toLowerCase();
                    if ((kl === 'estado' || kl === 'validacao' || kl === 'validado' || kl === 'origem' || (kl.includes('natureza') && valueTone(value))) && value) {
                      return (
                        <td key={col.key}>
                          <span className={`status-badge ${statusClass(value)}`}>{String(value)}</span>
                        </td>
                      );
                    }
                    const email = isConsultorPersonFieldKey(col.key)
                      ? (getFieldValue(row, 'ConsultorEmail') || getFieldValue(row, 'consultorEmail'))
                      : null;
                    return (
                      <td key={col.key} title={email || undefined}>
                        {formatCellValue(value, col.key, col.format)}
                      </td>
                    );
                  })}
                  {renderActions && (
                    <td className="col-acoes" onClick={e => e.stopPropagation()}>
                      {renderActions(row)}
                    </td>
                  )}
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
      {menu && menuCol && menu.anchor && ReactDOM.createPortal(
        <div
          className="data-grid-col-menu"
          ref={menuRef}
          style={{ top: menu.anchor.top, left: menu.anchor.left }}
          onMouseDown={e => e.stopPropagation()}
          onClick={e => e.stopPropagation()}
        >
          {menuCol.sortable !== false && (
            <div className="data-grid-col-menu-section">
              <button type="button" className="data-grid-col-menu-item" onClick={() => setSortDir(menu.key, 'asc')}>
                <Icon name="sortAsc" size={14} /> Ordenar A → Z
              </button>
              <button type="button" className="data-grid-col-menu-item" onClick={() => setSortDir(menu.key, 'desc')}>
                <Icon name="sortDesc" size={14} /> Ordenar Z → A
              </button>
              <button
                type="button"
                className="data-grid-col-menu-item"
                disabled={sort.key !== menu.key}
                onClick={clearSort}
              >
                <Icon name="sortClear" size={14} /> Remover ordenação
              </button>
            </div>
          )}
          {menuCol.filterable !== false && (
            <div className="data-grid-col-menu-section">
              <div className="data-grid-col-menu-title">Mostrar linhas onde:</div>
              <AppSelect
                layout="list"
                variant="compact"
                showMenuTitle={false}
                value={menu.draft.op1}
                options={GRID_FILTER_OPS}
                onChange={v => setMenu(m => ({ ...m, draft: { ...m.draft, op1: v } }))}
                ariaLabel="Primeira condição"
              />
              {needsValue1 && renderFilterValue(1)}
              <AppSelect
                layout="list"
                variant="compact"
                showMenuTitle={false}
                className="data-grid-menu-logic"
                value={menu.draft.logic}
                options={[{ value: 'and', label: 'e' }, { value: 'or', label: 'ou' }]}
                onChange={v => setMenu(m => ({ ...m, draft: { ...m.draft, logic: v } }))}
                ariaLabel="Lógica"
              />
              <AppSelect
                layout="list"
                variant="compact"
                showMenuTitle={false}
                value={menu.draft.op2}
                options={GRID_FILTER_OPS}
                onChange={v => setMenu(m => ({ ...m, draft: { ...m.draft, op2: v } }))}
                ariaLabel="Segunda condição"
              />
              {needsValue2 && renderFilterValue(2)}
              <div className="data-grid-col-menu-actions">
                <button type="button" className="btn btn-primary btn-sm" onClick={applyColumnFilter}>Filtrar</button>
                <button type="button" className="btn btn-secondary btn-sm" onClick={clearColumnFilter}>Limpar</button>
              </div>
            </div>
          )}
        </div>,
        document.body
      )}
    </div>
  );
}

function isDateFieldKey(fieldKey) {
  const key = (fieldKey || '').toLowerCase();
  return key.includes('data') || key.endsWith('date');
}

function isEmailFormField(key, label, colMeta) {
  const kind = formColumnKind(colMeta);
  if (kind === 'email') return true;
  const k = String(key || '').toLowerCase().replace(/_/g, '');
  if (k.includes('email') || k.includes('e-mail') || k.includes('correioelectron')) return true;
  const lb = foldFormLabel(label);
  return lb === 'email' || lb === 'e-mail' || lb.includes('email') || lb.includes('e-mail') || lb.includes('correio eletron');
}

function isValidEmailAddress(value) {
  const s = String(value || '').trim();
  if (!s) return true;
  if (s.length > 254 || /\s/.test(s)) return false;
  const at = s.indexOf('@');
  if (at < 1 || at !== s.lastIndexOf('@')) return false;
  const local = s.slice(0, at);
  const domain = s.slice(at + 1);
  if (!local || !domain || local.length > 64) return false;
  if (local.startsWith('.') || local.endsWith('.') || local.includes('..')) return false;
  if (domain.startsWith('-') || domain.startsWith('.') || domain.endsWith('.') || domain.includes('..')) return false;
  if (!domain.includes('.')) return false;
  return /^[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+$/.test(local)
    && /^[A-Za-z0-9.-]+$/.test(domain)
    && /\.[A-Za-z]{2,}$/.test(domain);
}

function emailFieldError(value) {
  const s = String(value || '').trim();
  if (!s) return '';
  return isValidEmailAddress(s) ? '' : 'Indique um email válido (ex.: nome@dominio.pt).';
}

function EmailInputField({ value, onChange, label, errorId }) {
  const raw = value == null ? '' : String(value);
  const error = emailFieldError(raw);
  const describedBy = error ? (errorId || 'email-field-error') : undefined;
  return (
    <>
      <input
        type="email"
        inputMode="email"
        autoComplete="email"
        spellCheck="false"
        value={raw}
        aria-label={label}
        aria-invalid={!!error}
        aria-describedby={describedBy}
        className={error ? 'is-invalid' : undefined}
        onChange={e => onChange(e.target.value)}
      />
      {error ? <p id={describedBy} className="form-hint form-hint-error">{error}</p> : null}
    </>
  );
}

function isContactoFormField(key, label, colMeta) {
  const kind = formColumnKind(colMeta);
  if (kind === 'phone' || kind === 'tel' || kind === 'telephone') return true;
  const k = String(key || '').toLowerCase();
  if (k.includes('email')) return false;
  if (
    k.includes('contacto') || k.includes('contato')
    || k.includes('telefone') || k.includes('telemovel') || k.includes('telem_x00f3')
    || k.includes('phone') || k.includes('mobile') || k.includes('celular')
  ) return true;
  const lb = foldFormLabel(label);
  if (lb.includes('email')) return false;
  return lb.includes('contacto') || lb.includes('contato')
    || lb.includes('telefone') || lb.includes('telemovel')
    || lb.includes('phone') || lb === 'tel';
}

function digitsOnly(value) {
  return String(value || '').replace(/\D/g, '');
}

function contactoFieldError(value) {
  const s = String(value || '').trim();
  if (!s) return '';
  return /\D/.test(s) ? 'O contacto só pode ter números.' : '';
}

function ContactoInputField({ value, onChange, label, maxLength = 15 }) {
  const raw = digitsOnly(value).slice(0, maxLength);
  return (
    <input
      type="tel"
      inputMode="numeric"
      autoComplete="tel"
      pattern="[0-9]*"
      maxLength={maxLength}
      value={raw}
      aria-label={label}
      onChange={e => onChange(digitsOnly(e.target.value).slice(0, maxLength))}
      onPaste={e => {
        e.preventDefault();
        const text = (e.clipboardData || window.clipboardData)?.getData('text') || '';
        onChange(digitsOnly(text).slice(0, maxLength));
      }}
    />
  );
}

function numericFormKind(key, label, colMeta) {
  const k = String(key || '').toLowerCase();
  const lb = foldFormLabel(label);
  const kind = formColumnKind(colMeta);
  const format = String(colMeta?.format || colMeta?.Format || '').toLowerCase();
  const labelRaw = String(label || '');
  const blob = `${k.replace(/_/g, ' ')} ${lb} ${kind} ${format}`;
  if (k.includes('probabil')) return null;
  const moneyHint = kind === 'currency'
    || format === 'money'
    || format === 'currency'
    || /\u20AC/.test(labelRaw)
    || /\b(euro|euros|eur)\b/.test(blob)
    || /\b(valor|montante|comissao|comiss|ticket)\b/.test(blob);
  const percentHint = format === 'percent'
    || /%/.test(labelRaw)
    || /\b(percentagem|percent|pct)\b/.test(blob)
    || k.includes('perc_')
    || k.includes('parcentagem');
  if (/\brendimento\b/.test(blob) || k.includes('rendimento')) {
    if (kind === 'currency' || (moneyHint && !percentHint)) return 'money';
    return 'percent';
  }
  if (percentHint && !moneyHint) return 'percent';
  if (moneyHint) return 'money';
  if (percentHint) return 'percent';
  return null;
}

function numericPayloadValue(raw, colMeta) {
  if (raw === '' || raw == null) return null;
  const n = parseNumericValue(raw);
  if (Number.isNaN(n)) return raw;
  const ck = formColumnKind(colMeta);
  if (ck === 'number' || ck === 'currency' || ck === 'integer' || ck === 'double') return n;
  return String(n);
}

function NumericInputField({ value, onChange, kind = 'number', label }) {
  const [draft, setDraft] = useState(null);
  const n = parseNumericValue(value);
  const hasValue = value !== '' && value != null && !Number.isNaN(n);
  const display = !hasValue
    ? ''
    : kind === 'money'
      ? formatCurrency(n)
      : kind === 'percent'
        ? formatPercent(n)
        : formatNumber(n);
  return (
    <input
      type="text"
      inputMode="decimal"
      aria-label={label}
      value={draft != null ? draft : display}
      onFocus={() => setDraft(hasValue ? String(n) : '')}
      onChange={e => setDraft(e.target.value)}
      onBlur={e => {
        const text = e.target.value;
        setDraft(null);
        const parsed = parseNumericValue(text);
        onChange(text === '' || Number.isNaN(parsed) ? '' : parsed);
      }}
      onKeyDown={e => {
        if (e.key === 'Enter') {
          e.preventDefault();
          e.currentTarget.blur();
        }
      }}
    />
  );
}

function isOperacaoFieldKey(fieldKey) {
  const key = String(fieldKey || '').toLowerCase();
  return (
    key.includes('ag_x00ea') ||
    key.includes('opera_x00e7') ||
    key === 'operacao' ||
    key === 'operação' ||
    (key.includes('agenc') && !key.includes('agenciaemail'))
  );
}

function isOperacaoFormField(fieldKey, label) {
  if (isOperacaoFieldKey(fieldKey)) return true;
  const lb = String(label || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
  return lb === 'operacao' || lb === 'agencia';
}

function isOperacaoMultiLookupField(fieldKey, label, colMeta) {
  const k = String(fieldKey || '').toLowerCase();
  const lb = foldFormLabel(label);
  const plural =
    lb === 'operacoes'
    || (k.includes('opera_x00e7') && (k.includes('x00f5') || k.endsWith('_es')));
  if (!isOperacaoFormField(fieldKey, label) && !plural) return false;
  const allowMultiple = colMeta?.allowMultiple === true || colMeta?.AllowMultiple === true;
  return allowMultiple || plural;
}

function isColaboradorPersonFormField(fieldKey, label) {
  const k = String(fieldKey || '').toLowerCase();
  if (k === 'colaborador' || (k.startsWith('colaborador') && !k.includes('email'))) return true;
  return foldFormLabel(label) === 'colaborador';
}

function isHiddenTitleFormField(fieldKey, label) {
  const k = String(fieldKey || '').toLowerCase();
  if (k === 'title' || k.startsWith('linktitle')) return true;
  return foldFormLabel(label) === 'titulo';
}

const PERMISSOES_CODES = ['C', 'R', 'U', 'D', 'A'];

function isPermissoesCodesField(fieldKey, label) {
  const k = String(fieldKey || '').toLowerCase();
  if (k === 'permiss_x00f5_es' || k === 'permissoes') return true;
  const lb = foldFormLabel(label);
  return lb === 'permissoes' || lb === 'permissao';
}

function isAcessoFuncaoFormField(fieldKey, label) {
  const k = String(fieldKey || '').toLowerCase();
  if (k === 'fun_x00e7__x00e3_o' || k === 'funcao') return true;
  return foldFormLabel(label) === 'funcao';
}

function isPermissoesFuncaoField(listKey, fieldKey, label) {
  return String(listKey || '').toLowerCase() === 'permissoes' && isAcessoFuncaoFormField(fieldKey, label);
}

function withPermissoesFuncaoColumn(listKey, defs) {
  if (String(listKey || '').toLowerCase() !== 'permissoes') return defs || [];
  const list = [...(defs || [])];
  if (list.some(c => isAcessoFuncaoFormField(c.key, c.label))) return list;
  const col = {
    key: 'Fun_x00e7__x00e3_o',
    label: 'Função',
    visible: true,
    order: 1,
    displayPath: 'value',
  };
  const idx = list.findIndex(c => isColaboradorPersonFormField(c.key, c.label));
  if (idx >= 0) list.splice(idx + 1, 0, col);
  else list.push(col);
  return list;
}

async function loadHimoFuncaoOptions() {
  const res = await apiFetch('/api/lists/himo-funcoes/items?pageSize=200');
  const items = res.items || res.Items || [];
  return items
    .map((i) => {
      const fields = getItemFields(i);
      return {
        id: String(i.id || i.Id || ''),
        label: String(fields.Title || fields.title || fields.Nome || i.id || ''),
      };
    })
    .filter(o => o.id)
    .sort((a, b) => a.label.localeCompare(b.label, 'pt', { sensitivity: 'base' }));
}

function memberFuncaoLookup(member) {
  if (!member) return { id: '', label: '' };
  const fields = member.fields || member.Fields || {};
  const raw = fields.Fun_x00e7__x00e3_o || fields.Funcao;
  const id = fields.Fun_x00e7__x00e3_oLookupId
    ?? fields.FuncaoLookupId
    ?? (raw && typeof raw === 'object' ? (raw.lookupId ?? raw.LookupId ?? raw.id ?? raw.Id) : null);
  const label = String(
    member.funcao
    || member.Funcao
    || (raw && typeof raw === 'object' ? (raw.lookupValue ?? raw.LookupValue ?? raw.value ?? raw.Title) : raw)
    || ''
  );
  return { id: id != null && id !== '' ? String(id) : '', label };
}

function matchHimoFuncaoOption(options, found) {
  const list = options || [];
  const id = String(found?.id || '').trim();
  if (id) {
    const byId = list.find(o => String(o.id) === id);
    if (byId) return byId;
  }
  const label = foldFormLabel(found?.label || '');
  if (!label) return null;
  return list.find(o => foldFormLabel(o.label) === label) || null;
}

async function resolveAcessoColaboradorEmail(form, colabKey) {
  const direct = String(form?.[`${colabKey}__email`] || '').trim();
  if (direct) return direct;
  const lookupId = String(form?.[colabKey] || '').trim();
  const name = String(form?.[`${colabKey}__label`] || '').trim();
  const q = name || lookupId;
  if (!q) return '';
  try {
    const params = new URLSearchParams({ take: '20', search: q });
    const result = await apiFetch(`/api/equipa/consultores?${params}`);
    const items = Array.isArray(result) ? result : (result.items || result.Items || []);
    const hit = items.find(h => String(h.lookupId ?? h.LookupId ?? '') === lookupId)
      || items.find(h => foldFormLabel(h.displayName || h.DisplayName) === foldFormLabel(name));
    return String(hit?.email || hit?.Email || '').trim();
  } catch {
    return '';
  }
}

async function findAcessoColaboradorMember(email, name) {
  const mail = String(email || '').trim();
  const label = String(name || '').trim();
  if (mail) {
    try {
      const res = await apiFetch(`/api/equipa/membro?email=${encodeURIComponent(mail)}`);
      const member = res.member || res.Member;
      if (member?.id || member?.Id) return member;
    } catch { /* try name search */ }
  }
  const q = label || (mail.includes('@') ? mail.split('@')[0].replace(/[._]/g, ' ') : mail);
  if (!q) return null;
  try {
    const res = await apiFetch(`/api/equipa/colaboradores?search=${encodeURIComponent(q)}`);
    const items = res.items || res.Items || [];
    const byEmail = mail
      ? items.find(m => foldFormLabel(m.email || m.Email) === foldFormLabel(mail))
      : null;
    if (byEmail) return byEmail;
    const byName = label
      ? items.find(m => foldFormLabel(m.assinatura || m.Assinatura) === foldFormLabel(label))
      : null;
    if (byName) return byName;
    if (items.length === 1) return items[0];
  } catch { /* ignore */ }
  return null;
}

async function syncAcessoColaboradorFuncao(fields) {
  const email = String(fields?.__acessoColaboradorEmail || '').trim();
  const name = String(fields?.__acessoColaboradorLabel || '').trim();
  const funcaoId = String(fields?.__acessoFuncaoLookupId || '').trim();
  if (!funcaoId) return { ok: false, reason: 'missing-funcao' };
  const member = await findAcessoColaboradorMember(email, name);
  const id = member?.id || member?.Id;
  if (!id) return { ok: false, reason: 'missing-colaborador' };
  await apiFetch(`/api/equipa/colaboradores/${encodeURIComponent(id)}`, {
    method: 'PATCH',
    body: JSON.stringify({ fields: { Fun_x00e7__x00e3_oLookupId: funcaoId } }),
  });
  return { ok: true, member };
}

function stripAcessoFuncaoExtras(fields) {
  const out = { ...(fields || {}) };
  delete out.__acessoColaboradorEmail;
  delete out.__acessoColaboradorLabel;
  delete out.__acessoFuncaoLookupId;
  return out;
}

function togglePermissaoCodes(selected, opt) {
  const cur = (Array.isArray(selected) ? selected : invChoiceSet(selected))
    .map(s => String(s).trim())
    .filter(Boolean);
  const code = String(opt || '').trim();
  if (!code) return cur;
  const has = cur.some(c => c.toLowerCase() === code.toLowerCase());
  if (/^a$/i.test(code)) return has ? [] : ['A'];
  const crud = cur.filter(c => !/^a$/i.test(c));
  if (has) return crud.filter(c => c.toLowerCase() !== code.toLowerCase());
  const canon = PERMISSOES_CODES.find(c => c.toLowerCase() === code.toLowerCase()) || code;
  return [...crud, canon];
}

function PermissoesCodesField({ value, onChange, label, extraChoices = [] }) {
  const stored = Array.isArray(value) ? value.filter(Boolean) : invChoiceSet(value);
  const isAdmin = stored.some(c => /^a$/i.test(c));
  const displayed = isAdmin ? PERMISSOES_CODES : stored;
  const options = [...new Set([...PERMISSOES_CODES, ...extraChoices])];
  return (
    <MultiChipField
      label={label}
      options={options}
      value={displayed}
      mapToggle={(_shown, id) => togglePermissaoCodes(stored, id)}
      onChange={onChange}
    />
  );
}

function resolveOperacaoLookupIds(item, fieldKey) {
  if (!item) return [];
  const fields = getItemFields(item);
  const fromId = fields[`${fieldKey}LookupId`];
  if (fromId != null && fromId !== '') {
    if (Array.isArray(fromId)) return fromId.map(String).filter(Boolean);
    return [String(fromId)];
  }
  return invLookupIds(getFieldValue(item, fieldKey));
}

const INV_OPERACAO_CHOICE_RE = /^(venda|arrendamento|trespasse)$/i;

function isInvDealTipoOperacaoField(listKey, key, label, colMeta) {
  const kind = formColumnKind(colMeta);
  if (kind === 'lookup') return false;
  const choices = formColumnChoices(colMeta);
  if (choices.some(c => INV_OPERACAO_CHOICE_RE.test(String(c).trim()))) return true;
  const lk = String(listKey || '').toLowerCase();
  if ((lk === 'ativos' || lk === 'oportunidades') && isOperacaoFormField(key, label)) return true;
  return false;
}

function invOperacaoChoiceOptions(colMeta) {
  return [...new Set([...INV_OPERACAO, ...formColumnChoices(colMeta)].filter(Boolean))];
}

function isConsultorFieldKey(fieldKey) {
  const key = String(fieldKey || '').toLowerCase();
  if (key.includes('valor_consultor') || key.includes('email') || key.includes('comiss')) return false;
  return key === 'consultor' || key === 'consultornome' || key.startsWith('consultor');
}

function isConsultorFormField(fieldKey, label) {
  if (isConsultorFieldKey(fieldKey)) return true;
  const lb = String(label || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
  if (lb.includes('comiss') || lb.includes('valor') || lb.includes('email')) return false;
  return lb === 'consultor' || lb === 'consultora' || lb.includes('consultor');
}

function isNaturezaFieldKey(fieldKey) {
  const key = String(fieldKey || '').toLowerCase();
  return key.includes('natureza') || key.includes('naturezaneg');
}

function isNaturezaFormField(fieldKey, label) {
  if (isNaturezaFieldKey(fieldKey)) return true;
  const lb = String(label || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
  return lb === 'natureza' || lb.startsWith('natureza ');
}

function isTipoClienteFormField(key, label) {
  const k = String(key || '').toLowerCase().replace(/_/g, '');
  if (k.includes('tipocliente') || k.includes('tipodecliente')) return true;
  const lb = foldFormLabel(label);
  return lb.includes('tipo') && lb.includes('cliente');
}

function invSingleChoiceOptions(key, label) {
  if (isNaturezaFormField(key, label)) return NATUREZA_CHOICE_OPTIONS;
  if (isTipoClienteFormField(key, label)) return INV_TIPO_CLIENTE;
  return [];
}

function isPlainSpLookupFormField(key, label, colMeta) {
  if (isConsultorFormField(key, label) || isOperacaoFormField(key, label) || isInvClientesLookupKey(key)) return false;
  const kind = formColumnKind(colMeta);
  if (isTipoClienteFormField(key, label)) return kind === 'lookup';
  if (kind === 'person') return false;
  return kind === 'lookup';
}

function resolveLookupFormValue(item, fieldKey, label) {
  if (!item) return '';
  const fields = getItemFields(item);
  const direct = fields[`${fieldKey}LookupId`];
  if (direct != null && String(direct) !== '') return String(direct);
  const fromRaw = invLookupIds(getFieldValue(item, fieldKey))[0];
  if (fromRaw) return String(fromRaw);
  if (isTipoClienteFormField(fieldKey, label)) {
    const altId = fields.TipodeClienteLookupId ?? fields.TipoClienteLookupId;
    if (altId != null && String(altId) !== '') return String(altId);
    const alt = getFieldValue(item, 'TipodeCliente') || getFieldValue(item, 'TipoCliente');
    const nested = invLookupIds(alt)[0];
    if (nested) return String(nested);
  }
  const wanted = String(fieldKey || '').toLowerCase();
  const idKey = Object.keys(fields).find((k) => {
    const kl = k.toLowerCase();
    if (!kl.endsWith('lookupid')) return false;
    const base = kl.slice(0, -8);
    if (base === wanted) return true;
    if (isTipoClienteFormField(fieldKey, label) && base.includes('tipo') && base.includes('cliente')) return true;
    return false;
  });
  if (idKey && fields[idKey] != null && String(fields[idKey]) !== '') return String(fields[idKey]);
  return '';
}

const KNOWN_LOOKUP_LIST_KEYS = {
  '42d1e1c2-b05c-4839-a0c6-12bf0788d264': 'inv-tipos-cliente',
};
let _enabledListKeyById = null;

async function lookupListKeyFromMeta(colMeta, fieldKey, label) {
  if (isTipoClienteFormField(fieldKey, label)) return 'inv-tipos-cliente';
  const id = String(colMeta?.lookupListId || colMeta?.LookupListId || '').toLowerCase();
  if (id && KNOWN_LOOKUP_LIST_KEYS[id]) return KNOWN_LOOKUP_LIST_KEYS[id];
  const name = foldFormLabel(colMeta?.lookupListName || colMeta?.LookupListName || '');
  if (name.includes('tipos cliente')) return 'inv-tipos-cliente';
  if (!id) return null;
  if (_enabledListKeyById == null) {
    try {
      const lists = await apiFetch('/api/settings/enabled-lists');
      const arr = Array.isArray(lists) ? lists : [];
      _enabledListKeyById = {};
      arr.forEach((l) => {
        const lid = String(l.listId || l.ListId || '').toLowerCase();
        const key = l.key || l.Key;
        if (lid && key) _enabledListKeyById[lid] = key;
      });
    } catch {
      _enabledListKeyById = {};
    }
  }
  return _enabledListKeyById[id] || null;
}

function ChoiceChipField({ options, value, onChange, label }) {
  const current = value == null ? '' : String(value);
  const opts = [];
  const seen = new Set();
  (options || []).forEach((o) => {
    const id = o && typeof o === 'object' ? String(o.id ?? o.value ?? '') : String(o || '');
    const text = o && typeof o === 'object' ? String(o.label ?? o.value ?? o.id ?? '') : String(o || '');
    if (!id || seen.has(id)) return;
    seen.add(id);
    opts.push({ id, label: text || id });
  });
  if (current && !seen.has(current)) opts.push({ id: current, label: current });
  return (
    <div className="inv-choice-grid" role="radiogroup" aria-label={label || undefined}>
      {opts.map(opt => (
        <button
          key={opt.id}
          type="button"
          className={`dash-prob-chip${current === opt.id ? ' active' : ''}`}
          aria-pressed={current === opt.id}
          onClick={() => onChange(current === opt.id ? '' : opt.id)}
        >
          {opt.label}
        </button>
      ))}
    </div>
  );
}

function MultiChipField({ options, value, onChange, label, mapToggle }) {
  const selected = (Array.isArray(value) ? value : []).map(String).filter(Boolean);
  const selectedSet = new Set(selected);
  const opts = [];
  const seen = new Set();
  (options || []).forEach((o) => {
    const id = o && typeof o === 'object' ? String(o.id ?? o.value ?? '') : String(o || '');
    const text = o && typeof o === 'object' ? String(o.label ?? o.value ?? o.id ?? '') : String(o || '');
    if (!id || seen.has(id)) return;
    seen.add(id);
    opts.push({ id, label: text || id });
  });
  selected.forEach((id) => {
    if (seen.has(id)) return;
    seen.add(id);
    opts.push({ id, label: id });
  });
  const toggle = (id) => {
    if (typeof mapToggle === 'function') {
      onChange(mapToggle(selected, id));
      return;
    }
    onChange(selectedSet.has(id) ? selected.filter(s => s !== id) : [...selected, id]);
  };
  return (
    <div className="inv-choice-grid" role="group" aria-label={label || undefined}>
      {opts.map(opt => (
        <button
          key={opt.id}
          type="button"
          className={`dash-prob-chip${selectedSet.has(opt.id) ? ' active' : ''}`}
          aria-pressed={selectedSet.has(opt.id)}
          onClick={() => toggle(opt.id)}
        >
          {opt.label}
        </button>
      ))}
    </div>
  );
}

function SpLookupChipField({ listKey: listKeyProp, colMeta, fieldKey, label, value, onChange, currentLabel }) {
  const [listKey, setListKey] = useState(
    listKeyProp || (isTipoClienteFormField(fieldKey, label) ? 'inv-tipos-cliente' : null)
  );
  const [options, setOptions] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      const resolved = listKeyProp || await lookupListKeyFromMeta(colMeta, fieldKey, label);
      if (!cancelled) setListKey(resolved || null);
    })();
    return () => { cancelled = true; };
  }, [listKeyProp, colMeta, fieldKey, label]);

  useEffect(() => {
    if (!listKey) {
      setLoading(false);
      return undefined;
    }
    let cancelled = false;
    (async () => {
      setLoading(true);
      try {
        const items = await fetchListItemsPages(listKey, { pageSize: 100, maxPages: 4 });
        if (cancelled) return;
        const opts = items.map((item) => {
          const id = String(item.id || item.Id || '');
          const fields = getItemFields(item);
          const title = String(fields.Title || fields.title || id);
          return { id, label: title };
        }).filter(o => o.id);
        setOptions(opts);
      } catch {
        if (!cancelled) setOptions([]);
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, [listKey]);

  const current = value == null ? '' : String(value);
  const opts = [...options];
  if (current && !opts.some(o => o.id === current)) {
    opts.unshift({ id: current, label: currentLabel || current });
  }
  if (loading && !opts.length) {
    return <p className="form-hint">A carregar opções…</p>;
  }
  if (!listKey && !opts.length) {
    return (
      <input
        type="text"
        value={currentLabel || current}
        onChange={e => onChange(e.target.value)}
        aria-label={label}
      />
    );
  }
  if (opts.length <= 8) {
    return (
      <ChoiceChipField
        label={label}
        options={opts}
        value={current}
        onChange={onChange}
      />
    );
  }
  return (
    <AppSelect
      value={current}
      emptyOption="—"
      options={opts.map(opt => ({ value: opt.id, label: opt.label }))}
      onChange={onChange}
      ariaLabel={label}
    />
  );
}

const NATUREZA_CHOICE_OPTIONS = ['Venda', 'Arrendamento'];

function resolveConsultorFormValue(item, fieldKey) {
  if (!item) return { lookupId: '', label: '', email: '' };
  const fields = getItemFields(item);
  const isConsultorKey = String(fieldKey || '').toLowerCase().startsWith('consultor');
  const lookupId =
    fields[`${fieldKey}LookupId`] ??
    (isConsultorKey ? fields.ConsultorLookupId : null) ??
    null;
  const raw = getFieldValue(item, fieldKey)
    || (isConsultorKey ? getFieldValue(item, 'Consultor') : null);
  let label = '';
  let id = lookupId != null && lookupId !== '' ? String(lookupId) : '';
  if (Array.isArray(lookupId) && lookupId.length) id = String(lookupId[0]);
  if (raw && typeof raw === 'object') {
    id = id || String(raw.id ?? raw.lookupId ?? raw.LookupId ?? '');
    label = String(raw.displayName ?? raw.DisplayName ?? raw.lookupValue ?? raw.LookupValue ?? raw.value ?? raw.Title ?? raw.email ?? raw.Email ?? '');
  } else if (raw != null && raw !== '') {
    label = String(raw);
  }
  const email = getFieldValue(item, `${fieldKey}Email`)
    || getFieldValue(item, 'ColaboradorEmail')
    || getFieldValue(item, 'ConsultorEmail')
    || (raw && (raw.email || raw.Email))
    || '';
  if (!label && email) label = String(email);
  return { lookupId: id, label, email: email ? String(email) : '' };
}

/** Searchable person picker — queries /api/equipa/consultores (SharePoint LookupId). */
function ConsultorSearchField({
  value,
  labelText,
  onChange,
  disabled,
  placeholder = 'Pesquisar e selecionar consultor…',
  ariaLabel = 'Consultor',
  hint = 'Escolha um resultado da lista (não basta escrever o nome).',
  emptyError = 'Sem consultores encontrados. Tente nome ou email.',
  clearLabel = 'Limpar consultor',
}) {
  const [query, setQuery] = useState(labelText || '');
  const [open, setOpen] = useState(false);
  const [loading, setLoading] = useState(false);
  const [hits, setHits] = useState([]);
  const [errorMsg, setErrorMsg] = useState('');
  const [menuPos, setMenuPos] = useState(null);
  const wrapRef = useRef(null);
  const inputRef = useRef(null);
  const timerRef = useRef(null);
  const reqIdRef = useRef(0);
  const hasSelection = !!(value && String(value) !== '0');

  useEffect(() => {
    setQuery(labelText || '');
  }, [labelText, value]);

  useEffect(() => {
    const onDoc = (e) => {
      if (wrapRef.current && !wrapRef.current.contains(e.target)
          && !e.target.closest?.('.consultor-search-menu')) {
        setOpen(false);
        if (!hasSelection) setQuery(labelText || '');
      }
    };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [hasSelection, labelText]);

  const syncMenuPos = useCallback(() => {
    const el = inputRef.current;
    if (!el) return;
    const r = el.getBoundingClientRect();
    const spaceBelow = window.innerHeight - r.bottom - 8;
    const spaceAbove = r.top - 8;
    const maxH = 240;
    const placeAbove = spaceBelow < 160 && spaceAbove > spaceBelow;
    const maxHeight = Math.max(120, Math.min(maxH, placeAbove ? spaceAbove : spaceBelow));
    setMenuPos({
      top: placeAbove ? Math.max(8, r.top - maxHeight - 4) : r.bottom + 4,
      left: r.left,
      width: Math.max(r.width, 260),
      maxHeight,
    });
  }, []);

  useLayoutEffect(() => {
    if (!open) return undefined;
    syncMenuPos();
    window.addEventListener('resize', syncMenuPos);
    window.addEventListener('scroll', syncMenuPos, true);
    return () => {
      window.removeEventListener('resize', syncMenuPos);
      window.removeEventListener('scroll', syncMenuPos, true);
    };
  }, [open, hits, loading, syncMenuPos]);

  const normalizeSearchText = (text) => String(text || '')
    .trim()
    .replace(/[.\u2026]+$/g, '')
    .trim();

  const pickHit = (h) => {
    if (!h) return;
    onChange({ lookupId: String(h.lookupId), label: h.displayName || h.email, email: h.email || '' });
    setQuery(h.displayName || h.email);
    setOpen(false);
    setErrorMsg('');
  };

  const runSearch = (text) => {
    if (timerRef.current) clearTimeout(timerRef.current);
    const reqId = ++reqIdRef.current;
    timerRef.current = setTimeout(async () => {
      setLoading(true);
      setErrorMsg('');
      setOpen(true);
      try {
        const params = new URLSearchParams({ take: '20' });
        const term = normalizeSearchText(text);
        if (term) params.set('search', term);
        const result = await apiFetch(`/api/equipa/consultores?${params}`);
        if (reqId !== reqIdRef.current) return;
        const list = Array.isArray(result) ? result : (result.items || result.Items || []);
        const mapped = list.map(h => ({
          lookupId: String(h.lookupId ?? h.LookupId ?? ''),
          displayName: h.displayName || h.DisplayName || '',
          email: h.email || h.Email || '',
        })).filter(h => h.lookupId && h.lookupId !== '0');
        setHits(mapped);
        if (mapped.length === 0 && term) {
          setErrorMsg(emptyError);
        }
      } catch (err) {
        if (reqId !== reqIdRef.current) return;
        setHits([]);
        const status = err?.status;
        if (status === 404) {
          setErrorMsg('Endpoint de consultores indisponível — reinicie a API MyHabita.');
        } else {
          setErrorMsg(err?.message || 'Erro ao pesquisar consultores.');
        }
      } finally {
        if (reqId === reqIdRef.current) setLoading(false);
      }
    }, 180);
  };

  const clearSelection = () => {
    if (disabled) return;
    onChange({ lookupId: '', label: '', email: '' });
    setQuery('');
    setHits([]);
    setOpen(false);
    inputRef.current?.focus();
  };

  return (
    <div className={`consultor-search${hasSelection ? ' has-selection' : ''}`} ref={wrapRef}>
      <div className="consultor-search-input-row">
        <input
          ref={inputRef}
          type="text"
          value={query}
          disabled={disabled}
          placeholder={placeholder}
          autoComplete="off"
          onFocus={() => { if (!disabled) runSearch(query); }}
          onChange={e => {
            if (disabled) return;
            const text = e.target.value;
            setQuery(text);
            if (value) onChange({ lookupId: '', label: '', email: '' });
            runSearch(text);
          }}
          onKeyDown={e => {
            if (e.key === 'Escape') {
              setOpen(false);
              return;
            }
            if (e.key === 'Enter' && open && hits[0]) {
              e.preventDefault();
              pickHit(hits[0]);
            }
            if (e.key === 'ArrowDown') {
              e.preventDefault();
              if (!open) runSearch(query);
            }
          }}
          aria-autocomplete="list"
          aria-expanded={open}
          aria-label={ariaLabel}
        />
        {hasSelection && !disabled && (
          <button
            type="button"
            className="consultor-search-clear"
            onClick={clearSelection}
            aria-label={clearLabel}
          >
            ×
          </button>
        )}
      </div>
      {hasSelection && labelText && (
        <div className="consultor-search-selected">Selecionado: {labelText}</div>
      )}
      {!hasSelection && !disabled && (
        <div className="consultor-search-hint">{hint}</div>
      )}
      {open && menuPos && ReactDOM.createPortal(
        <div
          className="consultor-search-menu consultor-search-menu-portal"
          role="listbox"
          style={{
            position: 'fixed',
            top: menuPos.top,
            left: menuPos.left,
            width: menuPos.width,
            maxHeight: menuPos.maxHeight,
            zIndex: 13000,
          }}
        >
          {loading && <div className="consultor-search-empty">A pesquisar…</div>}
          {!loading && hits.length === 0 && (
            <div className="consultor-search-empty">{errorMsg || 'Sem resultados'}</div>
          )}
          {!loading && hits.map(h => (
            <button
              key={h.lookupId}
              type="button"
              className="consultor-search-option"
              role="option"
              onMouseDown={(e) => e.preventDefault()}
              onClick={() => pickHit(h)}
            >
              <strong>{h.displayName || '—'}</strong>
              {h.email && <span>{h.email}</span>}
            </button>
          ))}
        </div>,
        document.body
      )}
    </div>
  );
}

function LocalLookupSearchField({
  options,
  value,
  labelText,
  onChange,
  placeholder = 'Pesquisar…',
  ariaLabel = 'Opção',
  hint = 'Pesquise e escolha um resultado.',
  emptyError = 'Sem resultados.',
  clearLabel = 'Limpar',
}) {
  const [query, setQuery] = useState(labelText || '');
  const [open, setOpen] = useState(false);
  const [menuPos, setMenuPos] = useState(null);
  const wrapRef = useRef(null);
  const inputRef = useRef(null);
  const hasSelection = !!(value && String(value) !== '0');
  const opts = options || [];

  useEffect(() => {
    setQuery(labelText || '');
  }, [labelText, value]);

  useEffect(() => {
    const onDoc = (e) => {
      if (wrapRef.current && !wrapRef.current.contains(e.target)
          && !e.target.closest?.('.consultor-search-menu')) {
        setOpen(false);
        if (!hasSelection) setQuery(labelText || '');
      }
    };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [hasSelection, labelText]);

  const syncMenuPos = useCallback(() => {
    const el = inputRef.current;
    if (!el) return;
    const r = el.getBoundingClientRect();
    const spaceBelow = window.innerHeight - r.bottom - 8;
    const spaceAbove = r.top - 8;
    const maxH = 240;
    const placeAbove = spaceBelow < 160 && spaceAbove > spaceBelow;
    const maxHeight = Math.max(120, Math.min(maxH, placeAbove ? spaceAbove : spaceBelow));
    setMenuPos({
      top: placeAbove ? Math.max(8, r.top - maxHeight - 4) : r.bottom + 4,
      left: r.left,
      width: Math.max(r.width, 260),
      maxHeight,
    });
  }, []);

  useLayoutEffect(() => {
    if (!open) return undefined;
    syncMenuPos();
    window.addEventListener('resize', syncMenuPos);
    window.addEventListener('scroll', syncMenuPos, true);
    return () => {
      window.removeEventListener('resize', syncMenuPos);
      window.removeEventListener('scroll', syncMenuPos, true);
    };
  }, [open, query, syncMenuPos]);

  const q = foldFormLabel(query);
  const hits = opts
    .filter(o => !q || foldFormLabel(o.label).includes(q))
    .slice(0, 20);

  const pickHit = (h) => {
    if (!h) return;
    onChange({ id: String(h.id), label: h.label || '' });
    setQuery(h.label || '');
    setOpen(false);
  };

  const clearSelection = () => {
    onChange({ id: '', label: '' });
    setQuery('');
    setOpen(false);
    inputRef.current?.focus();
  };

  return (
    <div className={`consultor-search${hasSelection ? ' has-selection' : ''}`} ref={wrapRef}>
      <div className="consultor-search-input-row">
        <input
          ref={inputRef}
          type="text"
          value={query}
          placeholder={placeholder}
          autoComplete="off"
          onFocus={() => { setOpen(true); syncMenuPos(); }}
          onChange={e => {
            const text = e.target.value;
            setQuery(text);
            if (value) onChange({ id: '', label: '' });
            setOpen(true);
          }}
          onKeyDown={e => {
            if (e.key === 'Escape') {
              setOpen(false);
              return;
            }
            if (e.key === 'Enter' && open && hits[0]) {
              e.preventDefault();
              pickHit(hits[0]);
            }
          }}
          aria-autocomplete="list"
          aria-expanded={open}
          aria-label={ariaLabel}
        />
        {hasSelection && (
          <button type="button" className="consultor-search-clear" onClick={clearSelection} aria-label={clearLabel}>
            ×
          </button>
        )}
      </div>
      {hasSelection && labelText && (
        <div className="consultor-search-selected">Selecionado: {labelText}</div>
      )}
      {!hasSelection && <div className="consultor-search-hint">{hint}</div>}
      {open && menuPos && ReactDOM.createPortal(
        <div
          className="consultor-search-menu consultor-search-menu-portal"
          role="listbox"
          style={{
            position: 'fixed',
            top: menuPos.top,
            left: menuPos.left,
            width: menuPos.width,
            maxHeight: menuPos.maxHeight,
            zIndex: 13000,
          }}
        >
          {hits.length === 0 && (
            <div className="consultor-search-empty">{emptyError}</div>
          )}
          {hits.map(h => (
            <button
              key={h.id}
              type="button"
              className={`consultor-search-option${String(h.id) === String(value) ? ' is-active' : ''}`}
              role="option"
              onMouseDown={(e) => e.preventDefault()}
              onClick={() => pickHit(h)}
            >
              <strong>{h.label || '—'}</strong>
            </button>
          ))}
        </div>,
        document.body
      )}
    </div>
  );
}

let _operacaoOptionsAllCache = null;
let _operacaoOptionsPromise = null;

function isOperacaoAtivaField(fields) {
  if (!fields || typeof fields !== 'object') return true;
  const raw = fields.Ativa ?? fields.Ativo ?? fields.ativa ?? fields.ativo ?? fields.Activo ?? fields.Active;
  if (raw === false || raw === 0) return false;
  if (raw === true || raw === 1) return true;
  const s = String(raw ?? '').trim().toLowerCase();
  if (!s) return true;
  if (['não', 'nao', 'false', '0', 'inativo', 'inactive', 'no'].includes(s)) return false;
  if (['sim', 'true', '1', 'ativo', 'ativa', 'activa', 'yes'].includes(s)) return true;
  return true;
}

async function loadAllOperacaoOptions() {
  if (_operacaoOptionsAllCache) return _operacaoOptionsAllCache;
  if (_operacaoOptionsPromise) return _operacaoOptionsPromise;

  _operacaoOptionsPromise = (async () => {
    try {
      const result = await apiFetch('/api/lists/operacoes/items?pageSize=100');
      const items = result.items || result.Items || [];
      let options = items.map(item => {
        const fields = getItemFields(item);
        const id = String(item.id || item.Id || '');
        const label =
          fields.Title ||
          fields.title ||
          fields.Local ||
          fields.Operacao ||
          fields.Nome ||
          id;
        return {
          id,
          label: String(label),
          active: isOperacaoAtivaField(fields),
        };
      }).filter(o => o.id && o.label);

      options = options.filter(o => o.active);
      options.sort((a, b) => a.label.localeCompare(b.label, 'pt'));
      _operacaoOptionsAllCache = options;
      return options;
    } catch {
      const fallback = ['Lisboa', 'Porto', 'Alentejo', 'Sines', 'Oeste'].map((label, i) => ({
        id: String(i + 1),
        label,
        active: true,
      }));
      _operacaoOptionsAllCache = fallback;
      return fallback;
    } finally {
      _operacaoOptionsPromise = null;
    }
  })();

  return _operacaoOptionsPromise;
}

async function loadOperacaoOptions(user) {
  const all = await loadAllOperacaoOptions();
  if (userIsAdmin(user)) return all;
  const allowed = user?.allowedOperacoes || user?.AllowedOperacoes || [];
  if (!Array.isArray(allowed) || !allowed.length) return all;
  const allowedSet = new Set(allowed.map(s => String(s).trim().toLowerCase()).filter(Boolean));
  const filtered = all.filter(o => allowedSet.has(String(o.label).toLowerCase()));
  return filtered.length ? filtered : all;
}

function resolveOperacaoFormValue(item, fieldKey) {
  if (!item) return '';
  const fields = getItemFields(item);
  const candidates = [
    fields[`${fieldKey}LookupId`],
    fields.Opera_x00e7__x00e3_oLookupId,
    fields.Ag_x00ea_nciaLookupId,
    fields.OperacaoLookupId,
  ];
  for (const c of candidates) {
    if (c != null && c !== '') return String(c);
  }
  const raw = getFieldValue(item, fieldKey);
  if (raw && typeof raw === 'object') {
    const id = raw.id ?? raw.lookupId ?? raw.Id;
    if (id != null && id !== '') return String(id);
    return String(raw.lookupValue ?? raw.value ?? raw.Title ?? raw.displayName ?? '');
  }
  return raw != null && raw !== '' ? String(raw) : '';
}

function GenericListItemPanel({ item, columns, columnDefs = [], user, onSave, onClose, saving, mode = 'edit', hideTitleField = false, listKey = null }) {
  const isCreate = mode === 'create';
  if (!isCreate && !item) return null;

  const isAdmin = userIsAdmin(user);
  const canChangeConsultor = userCanChangeConsultor(user);
  const hideTitle = hideTitleField || String(listKey || '').toLowerCase() === 'permissoes';
  const fields = isCreate ? {} : getItemFields(item);
  const canEdit = isCreate
    ? true
    : !!(user?.canUpdate || user?.CanUpdate || userIsAdmin(user) || userCanWriteInvestimentos(user, listKey));
  const titleKey = fields.Title != null ? 'Title' : (fields.title != null ? 'title' : 'Title');
  const titleLabel = Object.entries(columns).find(([, key]) =>
    key === 'Title' || key === 'title' || key.toLowerCase() === 'referencia' || key.includes('Refer')
  )?.[0] || 'Título';

  const [spCols, setSpCols] = useState([]);

  const fieldMeta = (key, label) => resolveFormFieldMeta(key, columnDefs, spCols, label);
  const rawFieldValue = (key) => (isCreate ? null : getFieldValue(item, key));
  const fieldIsLocation = (key, label) => isSpLocationField(key, label, rawFieldValue(key), fieldMeta(key, label));
  const fieldIsMulti = (key, label) => {
    if (isOperacaoMultiLookupField(key, label, fieldMeta(key, label))) return false;
    if (isColaboradorPersonFormField(key, label) || isConsultorFormField(key, label)) return false;
    if (isPermissoesCodesField(key, label)) return false;
    if (isPermissoesFuncaoField(listKey, key, label)) return false;
    if (String(listKey || '').toLowerCase() === 'ativos' && String(key).toLowerCase() === 'title') return true;
    return isInvMultiChoiceKey(key, label, rawFieldValue(key), fieldMeta(key, label));
  };
  const fieldIsLookup = (key, label) => {
    if (isPermissoesFuncaoField(listKey, key, label)) return false;
    return isPlainSpLookupFormField(key, label, fieldMeta(key, label));
  };

  const buildFormState = () => {
    const initial = {};
    Object.entries(columns).forEach(([label, key]) => {
      const raw = isCreate ? null : getFieldValue(item, key);
      if (fieldIsLocation(key, label)) {
        initial[key] = isCreate ? '' : (toLocationPayload(raw) || '');
      } else if (isOperacaoMultiLookupField(key, label, fieldMeta(key, label))) {
        initial[key] = isCreate ? [] : resolveOperacaoLookupIds(item, key);
      } else if (isPermissoesCodesField(key, label)) {
        initial[key] = isCreate ? [] : invChoiceSet(raw);
      } else if (fieldIsMulti(key, label)) {
        initial[key] = isCreate ? [] : invChoiceSet(raw);
      } else if (isInvClientesLookupKey(key)) {
        initial[key] = isCreate ? [] : invLookupIds(raw);
      } else if (isInvDealTipoOperacaoField(listKey, key, label, fieldMeta(key, label))) {
        initial[key] = isCreate ? '' : (toFormValue(raw, key) || '');
      } else if (isOperacaoFormField(key, label)) {
        initial[key] = isCreate ? '' : resolveOperacaoFormValue(item, key);
      } else if (isPermissoesFuncaoField(listKey, key, label)) {
        initial[key] = isCreate ? '' : resolveLookupFormValue(item, key, label);
      } else if (isColaboradorPersonFormField(key, label) || isConsultorFormField(key, label)) {
        const resolved = isCreate ? { lookupId: '', label: '', email: '' } : resolveConsultorFormValue(item, key);
        initial[key] = resolved.lookupId;
        initial[`${key}__label`] = resolved.label;
        initial[`${key}__email`] = resolved.email || '';
      } else if (fieldIsLookup(key, label)) {
        initial[key] = isCreate ? '' : resolveLookupFormValue(item, key, label);
      } else if (isEmailFormField(key, label, fieldMeta(key, label))) {
        initial[key] = isCreate ? '' : toFormValue(raw, key);
      } else if (isContactoFormField(key, label, fieldMeta(key, label))) {
        initial[key] = isCreate ? '' : digitsOnly(toFormValue(raw, key));
      } else {
        initial[key] = isCreate ? '' : toFormValue(raw, key);
      }
    });
    if (!hideTitle && !Object.values(columns).includes(titleKey) && !Object.values(columns).includes('Title') && !Object.values(columns).includes('title')) {
      initial[titleKey] = isCreate ? '' : toFormValue(fields[titleKey] ?? fields.Title ?? fields.title, titleKey);
    }
    if (isCreate) {
      Object.entries(columns).forEach(([, key]) => {
        if (isDateFieldKey(key) && !initial[key]) initial[key] = endOfMonthIso();
      });
      return applyDateDerivedToForm(initial, columns, pickPrimaryDateIso(initial, columns));
    }
    return initial;
  };

  const [form, setForm] = useState(buildFormState);
  const [operacaoOptions, setOperacaoOptions] = useState([]);
  const [funcaoOptions, setFuncaoOptions] = useState(null);
  const [saveError, setSaveError] = useState('');
  const prefilledRef = useRef(false);

  useEffect(() => {
    let cancelled = false;
    if (!listKey) {
      setSpCols([]);
      return undefined;
    }
    (async () => {
      try {
        const res = await apiFetch(`/api/settings/lists/by-key/${encodeURIComponent(listKey)}/columns`);
        const list = Array.isArray(res) ? res : (res?.columns || res?.Columns || []);
        if (!cancelled) setSpCols(Array.isArray(list) ? list : []);
      } catch {
        if (!cancelled) setSpCols([]);
      }
    })();
    return () => { cancelled = true; };
  }, [listKey]);

  useEffect(() => {
    prefilledRef.current = false;
    setForm(buildFormState());
  }, [item, mode]);

  useEffect(() => {
    if (String(listKey || '').toLowerCase() !== 'permissoes') return undefined;
    let cancelled = false;
    (async () => {
      try {
        const opts = await loadHimoFuncaoOptions();
        if (!cancelled) setFuncaoOptions(opts);
      } catch {
        if (!cancelled) setFuncaoOptions([]);
      }
    })();
    return () => { cancelled = true; };
  }, [listKey]);

  useEffect(() => {
    if (String(listKey || '').toLowerCase() !== 'permissoes') return undefined;
    if (funcaoOptions == null) return undefined;
    const colab = Object.entries(columns).find(([label, key]) => isColaboradorPersonFormField(key, label));
    if (!colab) return undefined;
    const colabKey = colab[1];
    const lookupId = String(form[colabKey] || '').trim();
    const name = String(form[`${colabKey}__label`] || '').trim();
    if (!lookupId && !name) return undefined;
    let cancelled = false;
    (async () => {
      try {
        const email = await resolveAcessoColaboradorEmail(form, colabKey);
        if (cancelled) return;
        const member = await findAcessoColaboradorMember(email, name);
        if (cancelled || !member) return;
        const found = memberFuncaoLookup(member);
        const match = matchHimoFuncaoOption(funcaoOptions, found);
        if (!match) return;
        setForm(prev => {
          if (String(prev[colabKey] || '') !== lookupId) return prev;
          if (prev.Fun_x00e7__x00e3_o) return prev;
          return {
            ...prev,
            Fun_x00e7__x00e3_o: match.id,
            Fun_x00e7__x00e3_o__label: match.label || found.label || '',
            [`${colabKey}__email`]: prev[`${colabKey}__email`] || email || member.email || member.Email || '',
          };
        });
      } catch { /* ignore missing colaborador row */ }
    })();
    return () => { cancelled = true; };
  }, [listKey, form.Colaborador, form.Colaborador__email, form.Colaborador__label, funcaoOptions]);

  const setFormWithDateDerived = (updater) => {
    setForm(prev => {
      const next = typeof updater === 'function' ? updater(prev) : { ...prev, ...updater };
      const iso = pickPrimaryDateIso(next, columns);
      return applyDateDerivedToForm(next, columns, iso);
    });
  };

  useEffect(() => {
    let cancelled = false;
    const entries = Object.entries(columns);
    const needsAgencyOperacao = entries.some(([label, key]) => (
      isOperacaoFormField(key, label) && !isInvDealTipoOperacaoField(listKey, key, label, fieldMeta(key, label))
    ));
    const needsConsultor = entries.some(([label, key]) => isConsultorFormField(key, label));

    (async () => {
      let opts = [];
      if (needsAgencyOperacao) {
        opts = await loadOperacaoOptions(user);
        if (cancelled) return;
        setOperacaoOptions(opts);
      }

      const self = needsConsultor ? await resolveSelfConsultorPrefill(user) : null;
      if (cancelled) return;

      setForm(prev => {
        let next = { ...prev };
        let changed = false;

        if (needsAgencyOperacao) {
          entries.forEach(([label, key]) => {
            if (!isOperacaoFormField(key, label)) return;
            if (isInvDealTipoOperacaoField(listKey, key, label, fieldMeta(key, label))) return;
            if (isOperacaoMultiLookupField(key, label, fieldMeta(key, label))) {
              const cur = Array.isArray(next[key]) ? next[key] : (next[key] ? [next[key]] : []);
              const mapped = cur.map((v) => {
                const s = String(v);
                if (opts.some(o => o.id === s)) return s;
                const byLabel = opts.find(o => o.label.toLowerCase() === s.toLowerCase());
                return byLabel ? byLabel.id : s;
              }).filter(Boolean);
              const same = mapped.length === cur.length && mapped.every((id, i) => id === String(cur[i]));
              if (!same) {
                next[key] = mapped;
                changed = true;
              }
              return;
            }
            const cur = next[key];
            if (cur) {
              if (opts.some(o => o.id === cur)) return;
              const byLabel = opts.find(o => o.label.toLowerCase() === String(cur).toLowerCase());
              if (byLabel) {
                next[key] = byLabel.id;
                changed = true;
              }
              return;
            }
            if (!isCreate) return;
            const def = pickDefaultOperacaoOption(opts, user);
            if (def) {
              next[key] = def.id;
              changed = true;
            }
          });
        }

        if (needsConsultor && self) {
          entries.forEach(([label, key]) => {
            if (!isConsultorFormField(key, label)) return;
            if (!isCreate && next[key]) return;
            if (next[key] && canChangeConsultor) return;
            next[key] = self.consultorLookupId;
            next[`${key}__label`] = self.consultorLabel;
            changed = true;
          });
        }

        return changed ? next : prev;
      });
      prefilledRef.current = true;
    })();

    return () => { cancelled = true; };
  }, [columns, user, isCreate, canChangeConsultor, item, mode]);

  const operacaoLocked = !isAdmin && operacaoOptions.length <= 1;

  const columnEntries = Object.entries(columns).filter(([label, key]) => {
    if (!hideTitle) return true;
    return !isHiddenTitleFormField(key, label);
  });
  const rangeGroups = collectRangeGroups(columnEntries);
  const rangeByAnchorKey = new Map();
  const rangeSkipKeys = new Set();
  const rangeByFieldKey = new Map();
  rangeGroups.forEach((group) => {
    rangeByAnchorKey.set(group.firstKey, group);
    [group.min, group.max].filter(Boolean).forEach((part) => {
      rangeByFieldKey.set(part.key, group);
      if (part.key !== group.firstKey) rangeSkipKeys.add(part.key);
    });
  });

  const handleSave = () => {
    const consultorEntry = Object.entries(columns).find(([label, key]) => isConsultorFormField(key, label));
    if (consultorEntry) {
      const [, consultorKey] = consultorEntry;
      if (!form[consultorKey]) {
        toast.warning('Selecione o consultor na lista (pesquisar e escolher).');
        return;
      }
    }
    const colaboradorEntry = Object.entries(columns).find(([label, key]) => isColaboradorPersonFormField(key, label));
    if (colaboradorEntry) {
      const [, colaboradorKey] = colaboradorEntry;
      if (!form[colaboradorKey]) {
        toast.warning('Selecione o colaborador na lista (pesquisar e escolher).');
        return;
      }
    }
    if (String(listKey || '').toLowerCase() === 'permissoes') {
      const funcaoEntry = Object.entries(columns).find(([label, key]) => isAcessoFuncaoFormField(key, label));
      const funcaoKey = funcaoEntry ? funcaoEntry[1] : 'Fun_x00e7__x00e3_o';
      if (!form[funcaoKey]) {
        toast.warning('Selecione a função.');
        return;
      }
    }
    const operacaoEntry = Object.entries(columns).find(([label, key]) => isOperacaoFormField(key, label));
    if (operacaoEntry) {
      const [opLabel, operacaoKey] = operacaoEntry;
      const multi = isOperacaoMultiLookupField(operacaoKey, opLabel, fieldMeta(operacaoKey, opLabel));
      if (!multi && !form[operacaoKey]) {
        toast.warning('Selecione a operação.');
        return;
      }
    }
    const permEntry = Object.entries(columns).find(([label, key]) => isPermissoesCodesField(key, label));
    if (permEntry) {
      const [, permKey] = permEntry;
      const codes = Array.isArray(form[permKey]) ? form[permKey].filter(Boolean) : invChoiceSet(form[permKey]);
      if (!codes.length) {
        toast.warning('Selecione as permissões.');
        return;
      }
    }
    const badEmail = Object.entries(columns).find(([label, key]) => {
      if (!isEmailFormField(key, label, fieldMeta(key, label))) return false;
      return !!emailFieldError(form[key]);
    });
    if (badEmail) {
      toast.warning(emailFieldError(form[badEmail[1]]) || 'Indique um email válido.');
      return;
    }
    const badContacto = Object.entries(columns).find(([label, key]) => {
      if (!isContactoFormField(key, label, fieldMeta(key, label))) return false;
      return !!contactoFieldError(form[key]);
    });
    if (badContacto) {
      toast.warning(contactoFieldError(form[badContacto[1]]) || 'O contacto só pode ter números.');
      return;
    }
    const payload = {};
    const rangeSaved = new Set();
    Object.entries(columns).forEach(([label, key]) => {
      if (hideTitle && isHiddenTitleFormField(key, label)) return;
      const meta = fieldMeta(key, label);
      if (meta.readOnly || (meta.isSystem && String(key).toLowerCase() !== 'title') || formColumnKind(meta) === 'calculated' || meta.calculatedFormula) return;
      const rangeGroup = rangeByFieldKey.get(key);
      if (rangeGroup) {
        if (rangeSaved.has(rangeGroup.stem)) return;
        rangeSaved.add(rangeGroup.stem);
        const sample = rangeGroup.min || rangeGroup.max;
        const spec0 = rangeSliderSpec(rangeGroup, fieldMeta(sample.key, sample.label));
        if (rangeGroup.min) {
          const lo = rangeThumbMin(form[rangeGroup.min.key], spec0.min);
          if (lo > spec0.min) payload[rangeGroup.min.key] = rangePayloadValue(spec0, lo);
          else if (!isCreate) payload[rangeGroup.min.key] = rangePayloadValue(spec0, spec0.min);
        }
        if (rangeGroup.max) {
          const raw = coerceRangeNumber(form[rangeGroup.max.key]);
          const hi = (raw == null || raw <= 0) ? spec0.max : raw;
          if (hi !== spec0.max) payload[rangeGroup.max.key] = rangePayloadValue(spec0, hi);
          else if (!isCreate) payload[rangeGroup.max.key] = rangePayloadValue(spec0, 0);
        }
        return;
      }
      if (isOperacaoMultiLookupField(key, label, fieldMeta(key, label))) {
        const ids = (Array.isArray(form[key]) ? form[key] : invLookupIds(form[key]))
          .map(n => parseInt(String(n), 10))
          .filter(n => !Number.isNaN(n));
        if (ids.length) payload[`${key}LookupId`] = ids;
        return;
      }
      if (isPermissoesCodesField(key, label)) {
        payload[key] = Array.isArray(form[key]) ? form[key].filter(Boolean) : invChoiceSet(form[key]);
        return;
      }
      if (isPermissoesFuncaoField(listKey, key, label)) {
        const spCol = findSpColByNames(spCols, [key, 'Fun_x00e7__x00e3_o', 'Funcao']);
        if (spCol && form[key]) {
          const spName = spCol.name || spCol.Name || key;
          const kind = formColumnKind(spCol);
          if (kind === 'lookup' || kind === 'person' || kind === 'user') payload[`${spName}LookupId`] = String(form[key]);
          else payload[spName] = form[`${key}__label`] || form[key];
        }
        return;
      }
      if (fieldIsMulti(key, label)) {
        const selected = Array.isArray(form[key]) ? form[key].filter(Boolean) : invChoiceSet(form[key]);
        if (!selected.length) return;
        const kind = formColumnKind(fieldMeta(key, label));
        if (kind === 'multichoice') payload[key] = selected;
        else if (kind === 'choice') payload[key] = selected[0];
        else payload[key] = JSON.stringify(selected);
        return;
      }
      if (fieldIsLocation(key, label)) {
        const loc = toLocationPayload(form[key]);
        if (!loc) return;
        const kind = formColumnKind(fieldMeta(key, label));
        if (kind === 'location' || kind === 'geolocation') payload[key] = loc;
        else payload[key] = loc.DisplayName || locationDisplayName(form[key]);
        return;
      }
      if (isInvClientesLookupKey(key)) {
        const ids = (Array.isArray(form[key]) ? form[key] : invLookupIds(form[key]))
          .map(n => parseInt(String(n), 10))
          .filter(n => !Number.isNaN(n));
        if (ids.length) payload.ClientesLookupId = ids;
        return;
      }
      if (isInvDealTipoOperacaoField(listKey, key, label, fieldMeta(key, label))) {
        payload[key] = String(form[key]);
        return;
      }
      if (form[key] === undefined || form[key] === '') return;
      if (isOperacaoFormField(key, label)) {
        payload[`${key}LookupId`] = String(form[key]);
        return;
      }
      if (isColaboradorPersonFormField(key, label) || isConsultorFormField(key, label)) {
        const kind = formColumnKind(fieldMeta(key, label));
        const idText = String(form[key] || '').trim();
        if (kind === 'lookup' || kind === 'person' || kind === 'user' || /^\d+$/.test(idText)) {
          payload[`${key}LookupId`] = idText;
        } else {
          const labelText = String(form[`${key}__label`] || '').trim();
          if (labelText) payload[key] = labelText;
        }
        return;
      }
      if (fieldIsLookup(key, label)) {
        const spName = fieldMeta(key, label).spName || key;
        payload[`${spName}LookupId`] = String(form[key]);
        return;
      }
      if (isEmailFormField(key, label, fieldMeta(key, label))) {
        payload[key] = String(form[key] || '').trim();
        return;
      }
      if (isContactoFormField(key, label, fieldMeta(key, label))) {
        const digits = digitsOnly(form[key]);
        if (digits) payload[key] = digits;
        return;
      }
      const numKind = numericFormKind(key, label, fieldMeta(key, label));
      if (numKind) {
        const parsed = numericPayloadValue(form[key], fieldMeta(key, label));
        if (parsed != null) payload[key] = parsed;
        return;
      }
      payload[key] = fromFormValue(form[key], key);
    });
    if (!hideTitle && payload[titleKey] === undefined && form[titleKey] !== undefined && form[titleKey] !== '')
      payload[titleKey] = form[titleKey];
    if (String(listKey || '').toLowerCase() === 'permissoes') {
      const colab = Object.entries(columns).find(([label, key]) => isColaboradorPersonFormField(key, label));
      const name = colab ? String(form[`${colab[1]}__label`] || '').trim() : '';
      if (!payload.Title && !payload.title) payload.Title = name || (isCreate ? 'Permissão' : undefined);
      if (!payload.Title) delete payload.Title;
      const funcaoEntry = Object.entries(columns).find(([label, key]) => isAcessoFuncaoFormField(key, label));
      payload.__acessoColaboradorEmail = colab ? String(form[`${colab[1]}__email`] || '').trim() : '';
      payload.__acessoColaboradorLabel = name;
      payload.__acessoFuncaoLookupId = String(form[funcaoEntry ? funcaoEntry[1] : 'Fun_x00e7__x00e3_o'] || '').trim();
    }
    const parts = dateDerivedParts(pickPrimaryDateIso(form, columns));
    if (parts) {
      [['Ano', parts.ano], ['M_x00ea_s', parts.mes], ['N_x00ba_semana', parts.numeroSemana]].forEach(([name, val]) => {
        const col = findSpColByNames(spCols, [name, name === 'M_x00ea_s' ? 'Mes' : name]);
        if (col && spColIsWritable(col)) payload[name] = val;
      });
    }
    setSaveError('');
    const finishSave = (body) => Promise.resolve(onSave(applyInvColumnAliases(listKey, body, spCols))).catch((err) => {
      setSaveError(err?.message || (isCreate ? 'Erro ao criar.' : 'Erro ao guardar.'));
    });
    if (String(listKey || '').toLowerCase() === 'permissoes' && !payload.__acessoColaboradorEmail) {
      const colab = Object.entries(columns).find(([label, key]) => isColaboradorPersonFormField(key, label));
      if (colab) {
        resolveAcessoColaboradorEmail(form, colab[1]).then((email) => {
          if (email) payload.__acessoColaboradorEmail = email;
          finishSave(payload);
        }).catch(() => finishSave(payload));
        return;
      }
    }
    finishSave(payload);
  };

  const renderField = (label, key) => {
    const value = isCreate ? null : getFieldValue(item, key);
    if (canEdit) {
      if (anoMesSemanaKind(key, label)) {
        return (
          <input
            type="text"
            value={form[key] ?? ''}
            readOnly
            disabled
            aria-label={`${label} (automático a partir da data)`}
          />
        );
      }
      if (isInvDealTipoOperacaoField(listKey, key, label, fieldMeta(key, label))) {
        return (
          <ChoiceChipField
            label={label}
            options={invOperacaoChoiceOptions(fieldMeta(key, label))}
            value={form[key] || ''}
            onChange={next => setForm({ ...form, [key]: next })}
          />
        );
      }
      if (isOperacaoMultiLookupField(key, label, fieldMeta(key, label))) {
        return (
          <MultiChipField
            label={label}
            options={operacaoOptions}
            value={form[key] || []}
            onChange={next => setForm({ ...form, [key]: next })}
          />
        );
      }
      if (isOperacaoFormField(key, label)) {
        const current = form[key] || '';
        const opts = [...operacaoOptions];
        if (
          current
          && !INV_OPERACAO_CHOICE_RE.test(String(current).trim())
          && !opts.some(o => o.id === current || o.label === current)
        ) {
          opts.unshift({ id: current, label: current, active: true });
        }
        const selectValue = opts.find(o => o.id === current || o.label === current)?.id || (
          INV_OPERACAO_CHOICE_RE.test(String(current).trim()) ? '' : current
        );
        return (
          <AppSelect
            value={selectValue}
            disabled={operacaoLocked}
            emptyOption={selectValue ? undefined : '—'}
            options={opts.map(o => ({ value: o.id, label: o.label }))}
            onChange={v => setForm({ ...form, [key]: v })}
            ariaLabel={label}
          />
        );
      }
      if (isColaboradorPersonFormField(key, label)) {
        return (
          <ConsultorSearchField
            value={form[key] || ''}
            labelText={form[`${key}__label`] || ''}
            placeholder="Pesquisar colaborador…"
            ariaLabel={label}
            hint="Escolha um resultado da lista (não basta escrever o email)."
            emptyError="Sem colaboradores encontrados. Tente nome ou email."
            clearLabel="Limpar colaborador"
            onChange={({ lookupId, label: display, email }) => {
              setForm({
                ...form,
                [key]: lookupId,
                [`${key}__label`]: display || '',
                [`${key}__email`]: email || '',
                Fun_x00e7__x00e3_o: '',
                Fun_x00e7__x00e3_o__label: '',
              });
            }}
          />
        );
      }
      if (isConsultorFormField(key, label)) {
        return (
          <ConsultorSearchField
            value={form[key] || ''}
            labelText={form[`${key}__label`] || ''}
            disabled={!canChangeConsultor}
            onChange={({ lookupId, label: display }) => {
              if (!canChangeConsultor) return;
              setForm({
                ...form,
                [key]: lookupId,
                [`${key}__label`]: display || '',
              });
            }}
          />
        );
      }
      if (isPermissoesFuncaoField(listKey, key, label)) {
        if (funcaoOptions == null) {
          return <div className="negocio-modal-readonly">A carregar funções…</div>;
        }
        if (!funcaoOptions.length) {
          return <div className="negocio-modal-readonly">Sem funções na lista HIMO Funções.</div>;
        }
        return (
          <LocalLookupSearchField
            options={funcaoOptions}
            value={form[key] || ''}
            labelText={form[`${key}__label`] || ''}
            placeholder="Pesquisar função…"
            ariaLabel={label}
            hint="Função do colaborador na app (lista HIMO Funções)."
            emptyError="Sem funções encontradas. Tente outro nome."
            clearLabel="Limpar função"
            onChange={({ id, label: display }) => {
              setForm({
                ...form,
                [key]: id,
                [`${key}__label`]: display || '',
              });
            }}
          />
        );
      }
      if (isPermissoesCodesField(key, label)) {
        return (
          <PermissoesCodesField
            label={label}
            extraChoices={fieldMeta(key, label).choices || []}
            value={form[key] || []}
            onChange={next => setForm({ ...form, [key]: next })}
          />
        );
      }
      if (fieldIsLocation(key, label)) {
        return (
          <LocationAddressField
            value={form[key] || ''}
            onChange={next => setForm({ ...form, [key]: next })}
          />
        );
      }
      if (fieldIsMulti(key, label)) {
        return (
          <InvMultiChoiceField
            fieldKey={key}
            label={label}
            extraChoices={fieldMeta(key, label).choices}
            value={form[key] || []}
            onChange={next => setForm({ ...form, [key]: next })}
          />
        );
      }
      if (isInvClientesLookupKey(key)) {
        return (
          <InvClientesPicker
            value={form[key] || []}
            onChange={next => setForm({ ...form, [key]: next })}
          />
        );
      }
      if (key.toLowerCase() === 'estado') {
        const estadoOpts = listKey === 'ativos'
          ? INV_ESTADO_ATIVO
          : listKey === 'oportunidades'
            ? INV_ESTADO_OPP
            : ['Em Curso', 'Concluído', 'Perdido'];
        return (
          <ChoiceChipField
            label={label}
            options={estadoOpts}
            value={form[key] || ''}
            onChange={next => setForm({ ...form, [key]: next })}
          />
        );
      }
      if (isNaturezaFormField(key, label)) {
        return (
          <AppSelect
            value={form[key] || ''}
            emptyOption="—"
            options={NATUREZA_CHOICE_OPTIONS}
            onChange={v => setForm({ ...form, [key]: v })}
            ariaLabel={label}
          />
        );
      }
      if (fieldIsLookup(key, label)) {
        const raw = isCreate ? null : getFieldValue(item, key);
        return (
          <SpLookupChipField
            colMeta={fieldMeta(key, label)}
            fieldKey={key}
            label={label}
            value={form[key] || ''}
            currentLabel={objectDisplayText(raw) || toFormValue(raw, key) || ''}
            onChange={next => setForm({ ...form, [key]: next })}
          />
        );
      }
      const meta = fieldMeta(key, label);
      const choiceOpts = [...new Set([
        ...invSingleChoiceOptions(key, label),
        ...meta.choices,
      ].filter(Boolean))];
      if (choiceOpts.length && formColumnKind(meta) === 'choice' && !isPermissoesCodesField(key, label) && !isPermissoesFuncaoField(listKey, key, label)) {
        const current = form[key] || '';
        const opts = [...new Set([...choiceOpts, current].filter(Boolean))];
        if (opts.length <= 8) {
          return (
            <ChoiceChipField
              label={label}
              options={opts}
              value={current}
              onChange={next => setForm({ ...form, [key]: next })}
            />
          );
        }
        return (
          <AppSelect
            value={current}
            emptyOption="—"
            options={opts.map(opt => ({ value: opt, label: opt }))}
            onChange={v => setForm({ ...form, [key]: v })}
            ariaLabel={label}
          />
        );
      }
      if (key.toLowerCase().includes('probabil')) {
        return (
          <AppSelect
            value={form[key] || ''}
            emptyOption="—"
            options={['Alta', 'Média', 'Baixa']}
            onChange={v => setForm({ ...form, [key]: v })}
            ariaLabel={label}
          />
        );
      }
      if (isDateFieldKey(key)) {
        return (
          <DatePickerField
            value={form[key] || ''}
            onChange={v => setFormWithDateDerived({ ...form, [key]: v })}
          />
        );
      }
      const numKind = numericFormKind(key, label, fieldMeta(key, label));
      if (numKind) {
        return (
          <NumericInputField
            kind={numKind}
            label={label}
            value={form[key]}
            onChange={next => setForm({ ...form, [key]: next })}
          />
        );
      }
      if (isEmailFormField(key, label, fieldMeta(key, label))) {
        return (
          <EmailInputField
            label={label}
            errorId={`${key}-email-error`}
            value={form[key] ?? ''}
            onChange={next => setForm({ ...form, [key]: next })}
          />
        );
      }
      if (isContactoFormField(key, label, fieldMeta(key, label))) {
        return (
          <ContactoInputField
            label={label}
            value={form[key] ?? ''}
            onChange={next => setForm({ ...form, [key]: next })}
          />
        );
      }
      const leftoverChoices = [...new Set([
        ...invSingleChoiceOptions(key, label),
        ...fieldMeta(key, label).choices,
      ].filter(Boolean))];
      if (leftoverChoices.length && !isPermissoesCodesField(key, label) && !isPermissoesFuncaoField(listKey, key, label)) {
        const current = form[key] || '';
        const opts = [...new Set([...leftoverChoices, current].filter(Boolean))];
        if (opts.length <= 8) {
          return (
            <ChoiceChipField
              label={label}
              options={opts}
              value={current}
              onChange={next => setForm({ ...form, [key]: next })}
            />
          );
        }
        return (
          <AppSelect
            value={current}
            emptyOption="—"
            options={opts.map(opt => ({ value: opt, label: opt }))}
            onChange={v => setForm({ ...form, [key]: v })}
            ariaLabel={label}
          />
        );
      }
      return (
        <input type="text" value={form[key] ?? ''} onChange={e => setForm({ ...form, [key]: e.target.value })} />
      );
    }
    if ((key.toLowerCase() === 'estado' || (key.toLowerCase().includes('natureza') && valueTone(value))) && value) {
      return <span className={`status-badge ${statusClass(value)}`}>{value}</span>;
    }
    return <div className="negocio-modal-readonly">{formatCellValue(value, key)}</div>;
  };

  const showExtraTitle = !hideTitle && !columnEntries.some(([, key]) => {
    const k = String(key).toLowerCase();
    return key === titleKey ||
      key === 'Title' ||
      key === 'title' ||
      k.includes('refer') ||
      k === 'title';
  });

  const heading = isCreate ? 'Novo registo' : (canEdit ? 'Atualizar registo' : 'Detalhe');
  const identEntry = columnEntries.find(([label]) => foldFormLabel(label).includes('identific'));
  const subtitle = isCreate
    ? 'Preencha os campos para criar um novo item'
    : (() => {
        if (identEntry) {
          const identVal = formatCellValue(getFieldValue(item, identEntry[1]), identEntry[1]);
          if (identVal && identVal !== '—') return identVal;
        }
        const rawTitle = fields.title || fields.Title || fields.Refer_x00ea_ncia || getFieldValue(item, 'Refer_x00ea_ncia');
        const pretty = objectDisplayText(rawTitle) || formatCellValue(rawTitle, titleKey);
        return (pretty && pretty !== '—') ? pretty : 'Sem título';
      })();

  return (
    <div className="negocio-modal-panel">
      <div className="negocio-modal-header">
        <div>
          <h3>{heading}</h3>
          <p className="negocio-modal-subtitle">{String(subtitle)}</p>
        </div>
        <button type="button" className="negocio-modal-close" onClick={onClose} aria-label="Fechar">
          <Icon name="close" size={18} />
        </button>
      </div>
      <div className="negocio-modal-body">
        {canEdit && showExtraTitle && (
          <div className="form-group">
            <label>{titleLabel}</label>
            <input
              type="text"
              value={form[titleKey] ?? ''}
              onChange={e => setForm({ ...form, [titleKey]: e.target.value })}
              autoFocus={isCreate}
            />
          </div>
        )}
        <div className="negocio-modal-grid-auto">
          {columnEntries.map(([label, key]) => {
            if (rangeSkipKeys.has(key)) return null;
            const rangeGroup = rangeByAnchorKey.get(key);
            if (rangeGroup) {
              const sample = rangeGroup.min || rangeGroup.max;
              const spec0 = rangeSliderSpec(rangeGroup, fieldMeta(sample.key, sample.label));
              const virtualMaxKey = rangeGroup.min && !rangeGroup.max ? `${rangeGroup.min.key}__rangeMax` : null;
              const storedMin = rangeGroup.min ? form[rangeGroup.min.key] : spec0.min;
              const storedMax = rangeGroup.max
                ? form[rangeGroup.max.key]
                : (virtualMaxKey ? form[virtualMaxKey] : spec0.max);
              const spec = expandSliderMax(spec0, storedMin, storedMax);
              const dual = !!(rangeGroup.min && (rangeGroup.max || virtualMaxKey));
              const lo = rangeThumbMin(rangeGroup.min ? storedMin : storedMax, spec.min);
              const hi = dual ? rangeThumbMax(storedMax, spec.max) : lo;
              const pretty = rangeSliderLabel(rangeGroup);
              if (!canEdit) {
                return (
                  <div className="form-group form-group-full" key={key}>
                    <label>{pretty}</label>
                    <div className="negocio-modal-readonly">
                      {dual ? `${spec.formatValue(lo)} – ${spec.formatValue(hi)}` : spec.formatValue(lo)}
                    </div>
                  </div>
                );
              }
              return (
                <div className="form-group form-group-full" key={key}>
                  <RangeSliderField
                    label={pretty}
                    min={spec.min}
                    max={spec.max}
                    step={spec.step}
                    minValue={lo}
                    maxValue={hi}
                    dual={dual}
                    formatValue={spec.formatValue}
                    onChange={(nextLo, nextHi) => {
                      const next = { ...form };
                      if (rangeGroup.min) next[rangeGroup.min.key] = nextLo;
                      if (rangeGroup.max) next[rangeGroup.max.key] = dual ? nextHi : nextLo;
                      else if (virtualMaxKey) next[virtualMaxKey] = nextHi;
                      setForm(next);
                    }}
                  />
                </div>
              );
            }
            return (
              <div
                className={`form-group${key.toLowerCase() === 'estado' || fieldIsMulti(key, label) || isInvClientesLookupKey(key) || isTipoClienteFormField(key, label) || isInvDealTipoOperacaoField(listKey, key, label, fieldMeta(key, label)) || isColaboradorPersonFormField(key, label) || isPermissoesFuncaoField(listKey, key, label) ? ' form-group-full' : ''}`}
                key={key}
              >
                <label>{label}</label>
                {renderField(label, key)}
              </div>
            );
          })}
        </div>
      </div>
      <div className="negocio-modal-footer">
        {saveError ? <p className="form-hint" style={{ color: '#C13C3C', marginRight: 'auto' }}>{saveError}</p> : null}
        <button type="button" className="btn btn-secondary" onClick={onClose}>Cancelar</button>
        {canEdit && (
          <button type="button" className="btn btn-primary" onClick={handleSave} disabled={saving}>
            {saving ? (isCreate ? 'A criar...' : 'A guardar...') : (isCreate ? 'Criar' : 'Guardar')}
          </button>
        )}
      </div>
    </div>
  );
}

function WalletMixSeg({ pct, tone, active, label, showLabel, onToggle }) {
  if (!(pct > 0)) return null;
  return (
    <div
      className={`comissao-wallet-seg is-${tone}${active ? ' is-active' : ''}`}
      style={{ flex: `0 0 ${pct}%` }}
      role="button"
      tabIndex={0}
      onClick={(e) => { e.stopPropagation(); onToggle(); }}
      onKeyDown={(e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          onToggle();
        }
      }}
    >
      {showLabel ? <span>{label}</span> : null}
    </div>
  );
}

function GenericListPage({
  user, listKey, title, icon, columns, hideHeader, headerSlot,
  onRowSelect = null, embedded = false, hideCreate = false, selectedRowId = null,
  gridStorageKey = null,
  onConfigureListColumns = null,
  hideTitleField = false,
  enableEstadoFilter = false,
  estadoFilterField = null,
  estadoFilterOptions = null,
  enableLiquidadoFilter = false,
  liquidadoFilterOptions = null,
  enablePeriodFilter = false,
  periodDateField = null,
  enableOperacaoFilter = false,
  walletMode = false,
  onCreateClick = null,
  syncFiltersToUrl = true,
  showMoneyTotals = false,
  defaultListView = 'table',
  matchAtivos = null,
  matchCriteria = null,
}) {
  const urlManagedKeys = useMemo(() => {
    const keys = ['q'];
    if (enableEstadoFilter) keys.push('estado');
    if (enableLiquidadoFilter) keys.push('liquidado');
    if (enablePeriodFilter) {
      keys.push('ano');
      keys.push('mes');
    }
    if (enableOperacaoFilter) keys.push('operacao');
    return keys;
  }, [enableEstadoFilter, enableLiquidadoFilter, enablePeriodFilter, enableOperacaoFilter]);

  const initialUrlFilters = useMemo(() => parsePageFiltersFromSearch(), []);
  const currentYear = String(new Date().getFullYear());
  const mesesNomes = ['Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'];

  const [data, setData] = useState([]);
  const [effectiveColumns, setEffectiveColumns] = useState(() => columnDefsToMap(toColumnDefs(columns)));
  const [columnDefs, setColumnDefs] = useState(() => toColumnDefs(columns));
  const [cardLayout, setCardLayout] = useState(() => emptyPhoneCardLayout());
  const [columnsFromSettings, setColumnsFromSettings] = useState(false);
  const [columnsLocked, setColumnsLocked] = useState(false);
  const [loading, setLoading] = useState(true);
  const [refreshing, setRefreshing] = useState(false);
  const [error, setError] = useState(null);
  const [selected, setSelected] = useState(null);
  const [activeRow, setActiveRow] = useState(null);
  const [creating, setCreating] = useState(false);
  const [saving, setSaving] = useState(false);
  const [pageToken, setPageToken] = useState(null);
  const [currentToken, setCurrentToken] = useState(null);
  const [prevTokens, setPrevTokens] = useState([]);
  const [totalCount, setTotalCount] = useState(null);
  const [search, setSearch] = useState(() => initialUrlFilters.q || '');
  const [appliedSearch, setAppliedSearch] = useState(() => initialUrlFilters.q || '');
  const [gridSort, setGridSort] = useState({ key: null, dir: 'asc' });
  const [gridColumnFilters, setGridColumnFilters] = useState({});
  const [perfectMatchOnly, setPerfectMatchOnly] = useState(false);
  const [estado, setEstado] = useState(() => (
    enableEstadoFilter
      ? (initialUrlFilters.estado || (walletMode ? 'Aprovado' : ''))
      : ''
  ));
  const [estadoChoices, setEstadoChoices] = useState(() => (
    Array.isArray(estadoFilterOptions) ? estadoFilterOptions.map(String) : []
  ));
  const [liquidado, setLiquidado] = useState(() => (enableLiquidadoFilter ? (initialUrlFilters.liquidado || '') : ''));
  const [liquidadoChoices, setLiquidadoChoices] = useState(() => (
    Array.isArray(liquidadoFilterOptions) ? liquidadoFilterOptions.map(String) : ['Sim', 'Não']
  ));
  const [filterAno, setFilterAno] = useState(() => {
    if (!enablePeriodFilter) return '';
    if (initialUrlFilters.ano) return initialUrlFilters.ano;
    return walletMode ? currentYear : '';
  });
  const [filterMes, setFilterMes] = useState(() => {
    if (!enablePeriodFilter) return '';
    if (walletMode) return '';
    return initialUrlFilters.mes || '';
  });
  const [operacao, setOperacao] = useState(() => initialUrlFilters.operacao || '');
  const [operacaoOptions, setOperacaoOptions] = useState([]);
  const [wallet, setWallet] = useState(null);
  const [walletLoading, setWalletLoading] = useState(false);
  const hasLoadedRef = useRef(false);
  const viewportMode = useViewportMode();
  const isPhone = viewportMode === 'phone';
  const layoutStorageKey = gridStorageKey || listKey || 'list';
  const inheritedDefault = (defaultListView === 'cards' || isPhone) ? 'cards' : 'table';
  const [listView, setListView] = useListLayoutView(layoutStorageKey, inheritedDefault);
  const useCards = listView === 'cards';

  const canUpdate = !!(user?.canUpdate || user?.CanUpdate || userIsAdmin(user) || userCanWriteInvestimentos(user, listKey));
  const canDelete = !!(user?.canDelete || user?.CanDelete || userIsAdmin(user));
  const canCreate = !!(user?.canCreate || user?.CanCreate || userIsAdmin(user) || userCanWriteInvestimentos(user, listKey));

  const resolvedEstadoField = (() => {
    if (estadoFilterField) return estadoFilterField;
    const fromCols = columnDefs.find(c => {
      const k = String(c.key || '').toLowerCase();
      const alias = String(c.aliasKey || '').toLowerCase();
      return k === 'estado' || alias === 'estado'
        || k === 'validado' || alias === 'validado'
        || k === 'validacao' || alias === 'validacao';
    });
    return fromCols?.key || 'Estado';
  })();

  const resolvedAnoField = useMemo(() => {
    const hit = columnDefs.find(c => String(c.key || '').toLowerCase() === 'ano');
    return hit?.key || 'Ano';
  }, [columnDefs]);

  const resolvedMesField = useMemo(() => {
    const hit = columnDefs.find(c => {
      const k = String(c.key || '').toLowerCase();
      return k === 'm_x00ea_s' || k === 'mes' || k === 'mês';
    });
    return hit?.key || 'M_x00ea_s';
  }, [columnDefs]);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      const { defs, fromSettings, locked, cardLayout: fetchedLayout } = await fetchListGridColumns(listKey, columns);
      if (!cancelled) {
        const nextDefs = withPermissoesFuncaoColumn(listKey, defs);
        setColumnDefs(nextDefs);
        setCardLayout(fetchedLayout || emptyPhoneCardLayout());
        setColumnsFromSettings(!!fromSettings);
        setColumnsLocked(!!(locked || fromSettings));
        setEffectiveColumns(columnDefsToMap(nextDefs));
      }
    })();
    return () => { cancelled = true; };
  }, [columns, listKey]);

  useEffect(() => {
    if (!enableEstadoFilter) return undefined;
    if (Array.isArray(estadoFilterOptions) && estadoFilterOptions.length) {
      setEstadoChoices(estadoFilterOptions.map(String));
      return undefined;
    }
    let cancelled = false;
    (async () => {
      const fromCol = columnDefs.find(c => {
        const k = String(c.key || '').toLowerCase();
        const alias = String(c.aliasKey || '').toLowerCase();
        const field = String(resolvedEstadoField || '').toLowerCase();
        return k === field || alias === field
          || k === 'estado' || alias === 'estado'
          || k === 'validado' || alias === 'validado'
          || k === 'validacao' || alias === 'validacao';
      });
      const localChoices = fromCol?.choices || fromCol?.Choices;
      if (Array.isArray(localChoices) && localChoices.length) {
        if (!cancelled) setEstadoChoices(localChoices.map(c => (typeof c === 'object' ? String(c.value ?? c.label ?? c) : String(c))));
        return;
      }
      try {
        const spCols = await apiFetch(`/api/settings/lists/by-key/${encodeURIComponent(listKey)}/columns`);
        const list = Array.isArray(spCols) ? spCols : (spCols?.columns || spCols?.Columns || []);
        const match = list.find(c => {
          const name = String(c.name || c.Name || c.key || c.Key || '').toLowerCase();
          const field = String(resolvedEstadoField || '').toLowerCase();
          return name === field || name === 'estado' || name === 'validado' || name === 'validacao';
        });
        const choices = match?.choices || match?.Choices || [];
        if (!cancelled && Array.isArray(choices) && choices.length) {
          setEstadoChoices(choices.map(c => (typeof c === 'object' ? String(c.value ?? c.label ?? c) : String(c))));
          return;
        }
      } catch { /* fallback below */ }
      if (!cancelled) {
        const field = String(resolvedEstadoField || '').toLowerCase();
        if (field.includes('valid')) setEstadoChoices(['Pendente', 'Aprovado', 'Reprovado']);
        else setEstadoChoices(['Em Curso', 'Concluído', 'Perdido']);
      }
    })();
    return () => { cancelled = true; };
  }, [enableEstadoFilter, estadoFilterOptions, listKey, columnDefs, resolvedEstadoField]);

  useEffect(() => {
    if (!enableLiquidadoFilter) return undefined;
    if (Array.isArray(liquidadoFilterOptions) && liquidadoFilterOptions.length) {
      setLiquidadoChoices(liquidadoFilterOptions.map(String));
      return undefined;
    }
    let cancelled = false;
    (async () => {
      const fromCol = columnDefs.find(c => {
        const k = String(c.key || '').toLowerCase();
        return k === 'liquidado';
      });
      const localChoices = fromCol?.choices || fromCol?.Choices;
      if (Array.isArray(localChoices) && localChoices.length) {
        if (!cancelled) setLiquidadoChoices(localChoices.map(c => (typeof c === 'object' ? String(c.value ?? c.label ?? c) : String(c))));
        return;
      }
      try {
        const spCols = await apiFetch(`/api/settings/lists/by-key/${encodeURIComponent(listKey)}/columns`);
        const list = Array.isArray(spCols) ? spCols : (spCols?.columns || spCols?.Columns || []);
        const match = list.find(c => String(c.name || c.Name || '').toLowerCase() === 'liquidado');
        const choices = match?.choices || match?.Choices || [];
        if (!cancelled && Array.isArray(choices) && choices.length) {
          setLiquidadoChoices(choices.map(c => (typeof c === 'object' ? String(c.value ?? c.label ?? c) : String(c))));
          return;
        }
      } catch { /* fallback below */ }
      if (!cancelled) setLiquidadoChoices(['Sim', 'Não']);
    })();
    return () => { cancelled = true; };
  }, [enableLiquidadoFilter, liquidadoFilterOptions, listKey, columnDefs]);

  useEffect(() => {
    if (!enableOperacaoFilter) return undefined;
    let cancelled = false;
    loadOperacaoOptions(user).then((opts) => {
      if (!cancelled) setOperacaoOptions(Array.isArray(opts) ? opts : []);
    }).catch(() => {
      if (!cancelled) setOperacaoOptions([]);
    });
    return () => { cancelled = true; };
  }, [enableOperacaoFilter, user]);

  useEffect(() => {
    const timer = setTimeout(() => {
      setAppliedSearch(prev => (prev === search ? prev : search));
      setPrevTokens([]);
    }, 400);
    return () => clearTimeout(timer);
  }, [search]);

  useEffect(() => {
    if (!syncFiltersToUrl) return undefined;
    replaceManagedPageFiltersInUrl({
      q: appliedSearch,
      estado: enableEstadoFilter ? estado : '',
      liquidado: enableLiquidadoFilter ? liquidado : '',
      ano: enablePeriodFilter ? filterAno : '',
      mes: enablePeriodFilter ? filterMes : '',
      operacao: enableOperacaoFilter ? operacao : '',
    }, urlManagedKeys);
    return undefined;
  }, [syncFiltersToUrl, appliedSearch, estado, liquidado, filterAno, filterMes, operacao, enableEstadoFilter, enableLiquidadoFilter, enablePeriodFilter, enableOperacaoFilter, urlManagedKeys]);

  const loadData = useCallback(async (token = null) => {
    const soft = hasLoadedRef.current;
    if (soft) setRefreshing(true);
    else setLoading(true);
    setError(null);
    try {
      const hasPeriod = enablePeriodFilter && (filterAno || filterMes);
      const params = new URLSearchParams({
        pageSize: ((enableEstadoFilter && estado) || (enableLiquidadoFilter && liquidado) || hasPeriod || (enableOperacaoFilter && operacao)) ? '100' : '24',
      });
      if (token) params.set('pageToken', token);
      if (appliedSearch.trim()) params.set('search', appliedSearch.trim());
      if (enableEstadoFilter && estado) {
        params.set('estado', estado);
        if (resolvedEstadoField) params.set('estadoField', resolvedEstadoField);
      }
      if (enableLiquidadoFilter && liquidado) {
        params.set('liquidado', liquidado);
        params.set('liquidadoField', 'liquidado');
      }
      if (enableOperacaoFilter && operacao) {
        params.set('operacao', operacao);
      }
      if (gridSort.key) {
        params.set('orderBy', `${gridSort.key} ${gridSort.dir === 'desc' ? 'desc' : 'asc'}`);
      }
      const activeColumnFilters = Object.fromEntries(
        Object.entries(gridColumnFilters || {}).filter(([, f]) => columnFilterIsActive(f))
      );
      if (enablePeriodFilter && periodDateField) {
        if (filterAno) params.set('ano', String(filterAno));
        if (filterMes) params.set('mes', String(filterMes));
        params.set('periodDateField', periodDateField);
      } else {
        if (enablePeriodFilter && filterAno) {
          activeColumnFilters[resolvedAnoField] = { op1: 'equals', val1: String(filterAno) };
        }
        if (enablePeriodFilter && filterMes) {
          activeColumnFilters[resolvedMesField] = { op1: 'equals', val1: String(filterMes) };
        }
      }
      if (Object.keys(activeColumnFilters).length > 0) {
        params.set('columnFilters', JSON.stringify(activeColumnFilters));
      }
      // Soft refresh (toolbar / re-fetch) asks server for additive SharePoint merge.
      if (soft && !token) params.set('refresh', 'true');

      const result = await apiFetch(`/api/lists/${encodeURIComponent(listKey)}/items?${params}`);
      let items = result.items || result.Items || [];
      setData(items);
      setColumnDefs(prev => {
        // Never overwrite curated settings / SP display-name columns with inferred FIELD_* maps.
        if ((columnsFromSettings || columnsLocked) && prev.length > 0) {
          setEffectiveColumns(columnDefsToMap(prev));
          return prev;
        }
        const fallbackMap = columnDefsToMap(prev.length ? prev : toColumnDefs(columns));
        const inferred = inferColumnsFromItems(items, fallbackMap);
        const next = toColumnDefs(inferred);
        setEffectiveColumns(columnDefsToMap(next));
        return next;
      });
      setPageToken(result.nextPageToken ?? result.NextPageToken ?? null);
      setTotalCount(result.totalCount ?? result.TotalCount ?? null);
      setCurrentToken(token);
      hasLoadedRef.current = true;
    } catch (err) {
      setError({ message: err.message, code: err.code, reconsentUrl: err.reconsentUrl });
      setData([]);
    } finally {
      setLoading(false);
      setRefreshing(false);
    }
  }, [listKey, appliedSearch, columns, columnsFromSettings, columnsLocked, enableEstadoFilter, estado, resolvedEstadoField, enableLiquidadoFilter, liquidado, enablePeriodFilter, periodDateField, filterAno, filterMes, resolvedAnoField, resolvedMesField, enableOperacaoFilter, operacao, gridSort, gridColumnFilters]);

  useEffect(() => {
    hasLoadedRef.current = false;
  }, [listKey]);

  useEffect(() => {
    loadData(null);
    setPrevTokens([]);
    setSelected(null);
    setActiveRow(null);
    setCreating(false);
  }, [loadData]);

  useEffect(() => {
    if (!walletMode) return undefined;
    let cancelled = false;
    setWalletLoading(true);
    const params = new URLSearchParams();
    if (filterAno) params.set('ano', String(filterAno));
    if (filterMes) params.set('mes', String(filterMes));
    if (enableOperacaoFilter && operacao) params.set('operacao', operacao);
    if (appliedSearch.trim()) params.set('search', appliedSearch.trim());
    apiFetch(`/api/mediacao/comissoes/wallet?${params}`)
      .then((res) => { if (!cancelled) setWallet(res); })
      .catch(() => { if (!cancelled) setWallet(null); })
      .finally(() => { if (!cancelled) setWalletLoading(false); });
    return () => { cancelled = true; };
  }, [walletMode, filterAno, filterMes, enableOperacaoFilter, operacao, appliedSearch]);

  const handleGridSortChange = useCallback((nextSort) => {
    setGridSort(nextSort?.key ? { key: nextSort.key, dir: nextSort.dir || 'asc' } : { key: null, dir: 'asc' });
    setPrevTokens([]);
  }, []);

  const handleGridColumnFilters = useCallback((nextFilters) => {
    setGridColumnFilters(nextFilters || {});
    setPrevTokens([]);
  }, []);

  const handleNextPage = () => {
    if (!pageToken || loading) return;
    setPrevTokens(prev => [...prev, currentToken]);
    loadData(pageToken);
  };

  const handlePrevPage = () => {
    if (prevTokens.length === 0 || loading) return;
    const tokens = [...prevTokens];
    const prevToken = tokens.pop();
    setPrevTokens(tokens);
    loadData(prevToken ?? null);
  };

  const handleSave = async (fields) => {
    if (!selected) return;
    setSaving(true);
    try {
      const id = selected.id || selected.Id;
      const extras = {
        __acessoColaboradorEmail: fields?.__acessoColaboradorEmail,
        __acessoColaboradorLabel: fields?.__acessoColaboradorLabel,
        __acessoFuncaoLookupId: fields?.__acessoFuncaoLookupId,
      };
      const payload = stripAcessoFuncaoExtras(fields);
      const updated = await apiFetch(`/api/lists/${encodeURIComponent(listKey)}/items/${id}`, {
        method: 'PATCH',
        body: JSON.stringify({ fields: payload }),
      });
      if (String(listKey || '').toLowerCase() === 'permissoes') {
        const synced = await syncAcessoColaboradorFuncao(extras);
        if (synced?.reason === 'missing-colaborador') {
          toast.warning('Permissão guardada. Função não gravada: este colaborador não está na lista HIMO Colaboradores.');
        }
      }
      setData(prev => prev.map(row => (row.id || row.Id) === id ? updated : row));
      setSelected(null);
      toast.success('Alterações guardadas.');
    } catch (err) {
      toast.error(err.message || 'Erro ao guardar.');
      throw err;
    } finally {
      setSaving(false);
    }
  };

  const handleCreate = async (fields) => {
    setSaving(true);
    try {
      const extras = {
        __acessoColaboradorEmail: fields?.__acessoColaboradorEmail,
        __acessoColaboradorLabel: fields?.__acessoColaboradorLabel,
        __acessoFuncaoLookupId: fields?.__acessoFuncaoLookupId,
      };
      const payload = stripAcessoFuncaoExtras(fields);
      await apiFetch(`/api/lists/${encodeURIComponent(listKey)}/items`, {
        method: 'POST',
        body: JSON.stringify({ fields: payload }),
      });
      if (String(listKey || '').toLowerCase() === 'permissoes') {
        const synced = await syncAcessoColaboradorFuncao(extras);
        if (synced?.reason === 'missing-colaborador') {
          toast.warning('Registo criado. Função não gravada: este colaborador não está na lista HIMO Colaboradores.');
        }
      }
      setCreating(false);
      setPrevTokens([]);
      await loadData(null);
      toast.success('Registo criado.');
    } catch (err) {
      toast.error(err.message || 'Erro ao criar.');
      throw err;
    } finally {
      setSaving(false);
    }
  };

  const handleDelete = async (id) => {
    if (!confirm('Eliminar este registo?')) return;
    try {
      await apiFetch(`/api/lists/${encodeURIComponent(listKey)}/items/${id}`, { method: 'DELETE' });
      setData(prev => prev.filter(row => (row.id || row.Id) !== id));
      if (selected && (selected.id || selected.Id) === id) setSelected(null);
      if (activeRow && (activeRow.id || activeRow.Id) === id) setActiveRow(null);
      toast.success('Registo eliminado.');
    } catch (err) {
      toast.error(err.message || 'Erro ao eliminar.');
    }
  };

  const openRowEditor = (row) => {
    if (!row) return;
    setActiveRow(row);
    setCreating(false);
    setSelected(row);
  };

  const highlightRow = (row) => {
    setActiveRow(row);
    onRowSelect?.(row);
  };

  const showAdminWallet = walletMode && (userIsAdmin(user) || userIsDiretorGeral(user));
  const [walletBucket, setWalletBucket] = useState(null);
  const resetWalletFilters = () => {
    setWalletBucket(null);
    setLiquidado('');
    setEstado('Aprovado');
  };

  const walletRecebidoActive = walletMode && walletBucket === 'recebido';
  const walletAReceberActive = walletMode && walletBucket === 'aReceber';
  const walletPendenteActive = walletMode && walletBucket === 'pendente';
  const walletHabitaFaturadoActive = walletMode && showAdminWallet && walletBucket === 'habitaFaturado';
  const walletHabitaNaoActive = walletMode && showAdminWallet && walletBucket === 'habitaNaoFaturado';
  const walletHabitaActive = walletMode && showAdminWallet && (
    walletBucket === 'habita' || walletHabitaFaturadoActive || walletHabitaNaoActive
  );

  const toggleWalletBucket = (bucket) => {
    if (walletBucket === bucket) {
      resetWalletFilters();
      setPrevTokens([]);
      return;
    }
    setWalletBucket(bucket);
    setEstado('Aprovado');
    if (bucket === 'recebido') setLiquidado('Sim');
    else if (bucket === 'aReceber' || bucket === 'pendente') setLiquidado('Não');
    else setLiquidado('');
    setPrevTokens([]);
  };

  const liquidadoChoiceLabel = (value) => {
    if (!walletMode) return value;
    const v = String(value || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
    if (v === 'sim' || v === 'yes') return 'Recebido';
    if (v === 'nao' || v === 'no') return 'Em falta';
    return value;
  };

  const walletPeriodLabel = (() => {
    const mesLabel = filterMes ? (mesesNomes[Number(filterMes) - 1] || '') : '';
    if (mesLabel && filterAno) return `${mesLabel} ${filterAno}`;
    if (filterAno) return String(filterAno);
    if (mesLabel) return mesLabel;
    return 'Todo o período';
  })();

  const walletCaption = (() => {
    if (operacao) return `Carteira ${operacao}`;
    if (showAdminWallet) return 'Carteira da operação';
    return 'A sua carteira';
  })();

  const walletNum = (key, fallback = 0) => {
    const raw = wallet?.[key] ?? wallet?.[key.charAt(0).toUpperCase() + key.slice(1)] ?? fallback;
    const n = Number(raw);
    return Number.isFinite(n) ? n : fallback;
  };

  const listToolbar = (
    <div className="toolbar mediacao-toolbar">
      <div className="toolbar-filters">
        {!userIsConsultor(user) && (
          <input
            placeholder="Pesquisar Consultor"
            aria-label="Pesquisar Consultor"
            value={search}
            onChange={e => setSearch(e.target.value)}
          />
        )}
        {enablePeriodFilter && (
          <AppSelect
            variant="toolbar"
            className="toolbar-filter-control"
            value={filterMes}
            emptyOption="Mês (todos)"
            options={mesesNomes.map((m, i) => ({ value: i + 1, label: m }))}
            onChange={v => { setFilterMes(v); setPrevTokens([]); }}
            ariaLabel="Mês"
          />
        )}
        {enablePeriodFilter && (
          <AppSelect
            variant="toolbar"
            className="toolbar-filter-control"
            value={filterAno}
            emptyOption="Ano (todos)"
            options={[...new Set([2024, 2025, 2026, 2027, Number(currentYear)])].sort((a, b) => a - b).map(y => ({ value: y, label: String(y) }))}
            onChange={v => { setFilterAno(v); setPrevTokens([]); }}
            ariaLabel="Ano"
          />
        )}
        {enableOperacaoFilter && (
          <AppSelect
            variant="toolbar"
            className="toolbar-filter-control"
            value={operacao}
            emptyOption="Operação (todas)"
            options={operacaoOptions.map(op => ({ value: op.label, label: op.label }))}
            onChange={v => { setOperacao(v); setPrevTokens([]); }}
            ariaLabel="Operação"
          />
        )}
        {enableEstadoFilter && (
          <AppSelect
            variant="toolbar"
            className="toolbar-filter-control"
            value={estado}
            emptyOption="Estado (todos)"
            options={estadoChoices}
            onChange={v => { setEstado(v); setPrevTokens([]); }}
            ariaLabel="Estado"
          />
        )}
        {enableLiquidadoFilter && (
          <AppSelect
            variant="toolbar"
            className="toolbar-filter-control"
            value={liquidado}
            emptyOption={walletMode ? 'Recebido (todos)' : 'Liquidado (todos)'}
            options={liquidadoChoices.map(s => ({ value: s, label: liquidadoChoiceLabel(s) }))}
            onChange={v => { setLiquidado(v); setPrevTokens([]); }}
            ariaLabel={walletMode ? 'Recebido' : 'Liquidado'}
          />
        )}
        <button
          type="button"
          className="btn btn-secondary toolbar-icon-btn"
          onClick={() => loadData(currentToken)}
          title="Atualizar"
        >
          <Icon name="refresh" size={16} />
        </button>
        <PhoneListViewToggle view={listView} onChange={setListView} />
      </div>
      <div className="toolbar-actions">
        <MapearColunasToolbarButton
          user={user}
          listKey={listKey}
          onConfigureListColumns={onConfigureListColumns}
        />
        {!hideCreate && (
          <button
            type="button"
            className="btn btn-secondary"
            disabled={!activeRow}
            onClick={() => activeRow && openRowEditor(activeRow)}
            title={activeRow
              ? (canUpdate ? 'Editar registo selecionado' : 'Ver registo selecionado')
              : 'Selecione um registo'}
          >
            <Icon name={canUpdate ? 'edit' : 'eye'} size={15} />
            <span className="btn-label-text">{canUpdate ? 'Editar' : 'Ver'}</span>
          </button>
        )}
        {canDelete && !hideCreate && (
          <button
            type="button"
            className="btn btn-secondary"
            disabled={!activeRow}
            onClick={() => activeRow && handleDelete(activeRow.id || activeRow.Id)}
            title={activeRow ? 'Eliminar registo selecionado' : 'Selecione um registo'}
          >
            <Icon name="trash" size={15} />
            <span className="btn-label-text">Eliminar</span>
          </button>
        )}
        {canCreate && !hideCreate && (
          <button
            type="button"
            className="btn btn-primary"
            onClick={() => {
              if (onCreateClick) {
                onCreateClick();
                return;
              }
              setSelected(null);
              setCreating(true);
            }}
          >
            <Icon name="plus" size={16} color="#FFFFFF" />
            <span className="btn-label-text">Novo</span>
          </button>
        )}
      </div>
    </div>
  );

  const resolvedGridColumns = useMemo(() => {
    const base = hideTitleField
      ? columnDefs.filter(c => {
          const k = String(c.key || '').toLowerCase();
          return k !== 'title' && k !== 'linktitle';
        })
      : columnDefs;
    const withoutVirtual = String(listKey || '').toLowerCase() === 'permissoes'
      ? base.filter(c => !isAcessoFuncaoFormField(c.key, c.label))
      : base;
    const labeled = applyFriendlyColumnLabels(withoutVirtual, listKey);
    if (!useCards && isPhone) return slimColumnsForPhone(labeled, 5);
    return labeled;
  }, [columnDefs, hideTitleField, isPhone, useCards, listKey]);

  const matchingMode = Array.isArray(matchAtivos);
  const matchScores = useMemo(() => {
    if (!matchingMode) return null;
    const map = new Map();
    (data || []).forEach(row => {
      map.set(String(row.id ?? row.Id), bestAtivoMatchScore(row, matchAtivos, matchCriteria));
    });
    return map;
  }, [matchingMode, matchAtivos, matchCriteria, data]);
  const perfectCount = useMemo(() => {
    if (!matchScores) return 0;
    let n = 0;
    matchScores.forEach(score => { if (score >= 100) n += 1; });
    return n;
  }, [matchScores]);
  const visibleRows = useMemo(() => {
    if (!perfectMatchOnly || !matchScores) return data;
    return (data || []).filter(row => (matchScores.get(String(row.id ?? row.Id)) || 0) >= 100);
  }, [data, perfectMatchOnly, matchScores]);
  const matchingTotals = matchingMode ? [
    {
      key: 'count',
      icon: 'home',
      title: 'Oportunidades',
      value: String((data || []).length),
      tip: perfectMatchOnly ? 'Mostrar todas as oportunidades' : 'Oportunidades nesta lista',
      onClick: perfectMatchOnly ? () => setPerfectMatchOnly(false) : undefined,
    },
    {
      key: 'match100',
      icon: 'trophy',
      title: '100% match',
      value: String(perfectCount),
      tip: perfectCount > 0
        ? (perfectMatchOnly ? 'Mostrar todas as oportunidades' : 'Filtrar oportunidades com match 100%')
        : 'Nenhuma oportunidade com match 100%',
      glow: perfectCount > 0,
      active: perfectMatchOnly,
      onClick: perfectCount > 0 ? () => setPerfectMatchOnly(on => !on) : undefined,
    },
  ] : [];

  const listHasTotalsCards = !walletMode && (matchingMode || listHasMoneyTotals(data, columnDefs));

  const listGrid = (
    <>
      {loading && <LoadingSpinner message="A carregar..." />}
      {error && <ErrorBanner error={error} onDismiss={() => setError(null)} />}
      {!loading && !error && (
        <>
          {listHasTotalsCards ? (
            <ListTotalsCards
              rows={data}
              columnDefs={columnDefs}
              extraCards={matchingTotals}
              includeMoney={!matchingMode}
            />
          ) : null}
          <div className={`module-page-table${refreshing ? ' is-refreshing' : ''}${useCards ? ' module-page-table-cards' : ''}`}>
          {refreshing && (
            <div className="data-grid-refresh-hint" aria-live="polite">A atualizar dados…</div>
          )}
          {useCards ? (
            <DataListCards
              listKey={listKey}
              columns={resolvedGridColumns}
              rows={visibleRows}
              cardLayout={cardLayout}
              selectedId={selectedRowId ?? activeRow?.id ?? activeRow?.Id ?? selected?.id ?? selected?.Id}
              onRowClick={highlightRow}
              onRowDoubleClick={hideCreate ? (onRowSelect || undefined) : openRowEditor}
              emptyMessage={perfectMatchOnly ? 'Sem oportunidades com match 100%.' : 'Sem registos.'}
              renderCardActions={hideCreate ? null : (row) => (
                <>
                  <button
                    type="button"
                    className="btn btn-secondary btn-sm"
                    onClick={() => openRowEditor(row)}
                    title={canUpdate ? 'Editar' : 'Ver'}
                  >
                    <Icon name={canUpdate ? 'edit' : 'eye'} size={14} />
                  </button>
                  {canDelete && (
                    <button
                      type="button"
                      className="btn btn-secondary btn-sm"
                      onClick={() => handleDelete(row.id || row.Id)}
                      title="Eliminar"
                    >
                      <Icon name="trash" size={14} />
                    </button>
                  )}
                </>
              )}
            />
          ) : (
            <DataGrid
              storageKey={gridStorageKey || listKey}
              columns={resolvedGridColumns}
              rows={visibleRows}
              selectedId={selectedRowId ?? activeRow?.id ?? activeRow?.Id ?? selected?.id ?? selected?.Id}
              onRowClick={highlightRow}
              onRowDoubleClick={hideCreate ? (onRowSelect || undefined) : openRowEditor}
              emptyMessage={perfectMatchOnly ? 'Sem oportunidades com match 100%.' : 'Sem registos.'}
              filterMode="server"
              preferSourceOrder={columnsFromSettings || columnsLocked}
              showColumnChooser={!isPhone}
              onColumnFiltersChange={handleGridColumnFilters}
              onSortChange={handleGridSortChange}
              onGlobalQueryChange={(q) => {
                setSearch(String(q || ''));
                setPrevTokens([]);
              }}
            />
          )}
          <div className="pagination">
            <span>
              Linhas: {visibleRows.length}{totalCount != null ? ` / ${totalCount}` : ''}
              {' · '}
              Página {prevTokens.length + 1}
            </span>
            <span>
              <button type="button" className="btn btn-secondary btn-icon" onClick={handlePrevPage} disabled={prevTokens.length === 0 || loading} title="Anterior">
                <Icon name="arrowLeft" size={16} />
              </button>
              {' '}
              <button type="button" className="btn btn-secondary btn-icon" onClick={handleNextPage} disabled={!pageToken || loading} title="Seguinte">
                <Icon name="arrowRight" size={16} />
              </button>
            </span>
          </div>
        </div>
        </>
      )}
    </>
  );

  const walletRecebidoVal = walletNum('recebido');
  const walletAReceberVal = walletNum('aReceber');
  const walletHabitaVal = walletNum('habita');
  const walletHabitaFaturadoVal = walletNum('habitaFaturado');
  const walletHabitaNaoVal = walletNum('habitaNaoFaturado');
  const walletHabitaSplit = walletHabitaFaturadoVal + walletHabitaNaoVal;
  const walletHabitaBarTotal = walletHabitaSplit > 0 ? walletHabitaSplit : walletHabitaVal;
  const walletHabitaFaturadoPct = walletHabitaBarTotal > 0 ? (walletHabitaFaturadoVal / walletHabitaBarTotal) * 100 : 0;
  const walletHabitaNaoPct = walletHabitaBarTotal > 0 ? (walletHabitaNaoVal / walletHabitaBarTotal) * 100 : (walletHabitaVal > 0 ? 100 : 0);
  const walletConsultorTotal = walletRecebidoVal + walletAReceberVal;
  const walletRecebidoPct = walletConsultorTotal > 0 ? (walletRecebidoVal / walletConsultorTotal) * 100 : 0;
  const walletAReceberPct = walletConsultorTotal > 0 ? (walletAReceberVal / walletConsultorTotal) * 100 : 0;

  const walletStrip = walletMode ? (
    <div className="comissao-wallet">
      <p className="comissao-wallet-caption">
        {walletCaption}
        <span> · {walletPeriodLabel}</span>
      </p>
      <div className={`comissao-wallet-row${showAdminWallet ? ' is-admin' : ''}`}>
        <DashCard
          icon="home"
          title="A receber"
          titleTip="Aprovado e ainda não liquidado"
          className={`comissao-wallet-card${walletAReceberActive ? ' is-active' : ''}`}
          disableCompact
          onExplore={() => toggleWalletBucket('aReceber')}
          exploreLabel="ver"
          onClick={() => toggleWalletBucket('aReceber')}
          role="button"
          tabIndex={0}
          onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleWalletBucket('aReceber'); } }}
        >
          <div className="kpi-card-v2-value">{walletLoading ? '…' : formatCurrency(walletNum('aReceber'))}</div>
          <p className="kpi-trend neutral">{walletNum('aReceberCount')} comissões</p>
        </DashCard>
        <DashCard
          icon="trophy"
          title="Recebido"
          titleTip="Aprovado e já liquidado"
          className={`comissao-wallet-card${walletRecebidoActive ? ' is-active' : ''}`}
          disableCompact
          onExplore={() => toggleWalletBucket('recebido')}
          exploreLabel="ver"
          onClick={() => toggleWalletBucket('recebido')}
          role="button"
          tabIndex={0}
          onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleWalletBucket('recebido'); } }}
        >
          <div className="kpi-card-v2-value">{walletLoading ? '…' : formatCurrency(walletNum('recebido'))}</div>
          <p className="kpi-trend positive">{walletNum('recebidoCount')} comissões</p>
        </DashCard>
        <DashCard
          icon="activity"
          title="Pendente"
          titleTip="Aprovado e ainda não liquidado"
          className={`comissao-wallet-card${walletPendenteActive ? ' is-active' : ''}`}
          disableCompact
          onExplore={() => toggleWalletBucket('pendente')}
          exploreLabel="ver"
          onClick={() => toggleWalletBucket('pendente')}
          role="button"
          tabIndex={0}
          onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleWalletBucket('pendente'); } }}
        >
          <div className="kpi-card-v2-value">{walletLoading ? '…' : formatCurrency(walletNum('pendente'))}</div>
          <p className="kpi-trend neutral">{walletNum('pendenteCount')} comissões</p>
        </DashCard>
        {showAdminWallet ? (
          <DashCard
            icon="graph"
            title="HABITA"
            titleTip="Comissão HABITA aprovada no período"
            className={`comissao-wallet-card${walletHabitaActive ? ' is-active' : ''}`}
            disableCompact
            onExplore={() => toggleWalletBucket('habita')}
            exploreLabel="ver"
            onClick={() => toggleWalletBucket('habita')}
            role="button"
            tabIndex={0}
            onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleWalletBucket('habita'); } }}
          >
            <div className="kpi-card-v2-value">{walletLoading ? '…' : formatCurrency(walletNum('habita'))}</div>
            <p className="kpi-trend neutral">{walletNum('habitaCount')} comissões</p>
          </DashCard>
        ) : null}
      </div>
      <div className="comissao-wallet-mix">
        <div className="comissao-wallet-mix-row">
          <span className="comissao-wallet-mix-label">Consultor</span>
          <AppTooltip label={`Recebido ${formatCurrency(walletRecebidoVal)} · A receber ${formatCurrency(walletAReceberVal)}`}>
            <div
              className="comissao-wallet-track"
              role="img"
              aria-label={`Recebido ${formatCurrency(walletRecebidoVal)}, a receber ${formatCurrency(walletAReceberVal)}`}
            >
              <WalletMixSeg
                pct={walletRecebidoPct}
                tone="green"
                active={walletRecebidoActive}
                label={`Recebido ${formatCurrency(walletRecebidoVal)}`}
                showLabel={walletRecebidoPct >= 22}
                onToggle={() => toggleWalletBucket('recebido')}
              />
              <WalletMixSeg
                pct={walletAReceberPct}
                tone="navy"
                active={walletAReceberActive || walletPendenteActive}
                label={`A receber ${formatCurrency(walletAReceberVal)}`}
                showLabel={walletAReceberPct >= 22}
                onToggle={() => toggleWalletBucket('aReceber')}
              />
            </div>
          </AppTooltip>
          <strong className="comissao-wallet-mix-total">{walletLoading ? '…' : formatCurrency(walletConsultorTotal)}</strong>
        </div>
        {showAdminWallet ? (
          <div className="comissao-wallet-mix-row">
            <span className="comissao-wallet-mix-label">HABITA</span>
            <AppTooltip label={`Faturado ${formatCurrency(walletHabitaFaturadoVal)} · Não faturado ${formatCurrency(walletHabitaNaoVal)}`}>
              <div
                className={`comissao-wallet-track${walletHabitaActive && walletBucket === 'habita' ? ' is-active' : ''}`}
                role="img"
                aria-label={`Faturado ${formatCurrency(walletHabitaFaturadoVal)}, não faturado ${formatCurrency(walletHabitaNaoVal)}`}
              >
                <WalletMixSeg
                  pct={walletHabitaFaturadoPct}
                  tone="green"
                  active={walletHabitaFaturadoActive || walletBucket === 'habita'}
                  label={`Faturado ${formatCurrency(walletHabitaFaturadoVal)}`}
                  showLabel={walletHabitaFaturadoPct >= 22}
                  onToggle={() => toggleWalletBucket('habitaFaturado')}
                />
                <WalletMixSeg
                  pct={walletHabitaNaoPct}
                  tone="navy"
                  active={walletHabitaNaoActive || walletBucket === 'habita'}
                  label={`Não faturado ${formatCurrency(walletHabitaNaoVal)}`}
                  showLabel={walletHabitaNaoPct >= 22}
                  onToggle={() => toggleWalletBucket('habitaNaoFaturado')}
                />
              </div>
            </AppTooltip>
            <strong className="comissao-wallet-mix-total">{walletLoading ? '…' : formatCurrency(walletHabitaVal)}</strong>
          </div>
        ) : null}
        <div className="comissao-wallet-bar-legend">
          <span><i className="dash-swatch is-won" /> Recebido <em>{formatCurrency(walletRecebidoVal)}</em></span>
          <span><i className="dash-swatch is-pipe" /> A receber <em>{formatCurrency(walletAReceberVal)}</em></span>
          {showAdminWallet ? (
            <>
              <span><i className="dash-swatch is-won" /> Faturado <em>{formatCurrency(walletHabitaFaturadoVal)}</em></span>
              <span><i className="dash-swatch is-pipe" /> Não faturado <em>{formatCurrency(walletHabitaNaoVal)}</em></span>
            </>
          ) : null}
        </div>
      </div>
    </div>
  ) : null;

  const shell = (
    <ModulePageShell tabs={headerSlot || null} toolbar={listToolbar}>
      {walletStrip}
      {listGrid}
    </ModulePageShell>
  );

  const inner = (
    <>
      {!hideHeader && <ModuleTopbar icon={icon} title={title} user={user} />}
      {embedded ? (
        <div className="generic-list-embedded">
          {headerSlot}
          <div className="module-page-shell-toolbar">{listToolbar}</div>
          <div className="module-page-shell-body">{listGrid}</div>
        </div>
      ) : (
        <div className="page-body">
          <div className="page-content module-layout">
            <div className="content-main">
              {shell}
            </div>
          </div>
        </div>
      )}

      <NegocioModalOverlay
        open={creating}
        onClose={() => { if (!saving) setCreating(false); }}
        label="Novo registo"
      >
        <GenericListItemPanel
          item={null}
          columns={effectiveColumns}
          columnDefs={columnDefs}
          user={user}
          listKey={listKey}
          mode="create"
          onSave={handleCreate}
          onClose={() => setCreating(false)}
          saving={saving}
          hideTitleField={hideTitleField}
        />
      </NegocioModalOverlay>

      <NegocioModalOverlay
        open={!!selected && !creating}
        onClose={() => { if (!saving) setSelected(null); }}
        label="Atualizar registo"
      >
        <GenericListItemPanel
          item={selected}
          columns={effectiveColumns}
          columnDefs={columnDefs}
          user={user}
          listKey={listKey}
          mode="edit"
          onSave={handleSave}
          onClose={() => setSelected(null)}
          saving={saving}
          hideTitleField={hideTitleField}
        />
      </NegocioModalOverlay>
    </>
  );
  return inner;
}

const SP_PIPELINE_COLUMNS = {
  'Operação': 'Ag_x00ea_ncia',
  'Consultor': 'Consultor',
  'Estado': 'Estado',
  'Prob.': 'ProbabilidadedeConcretiza_x00e7__x00e3_o',
  'Natureza': 'Naturezadeneg_x00f3_cio',
  'Comissão': 'Comiss_x00e3_oTotalHABITA',
  'Data': 'Data',
  'Faturação': 'DataPrevista_x002f_EfetivaConclu',
  'Recebimento': 'Dataprevistaderecebimento',
  'Referência': 'Refer_x00ea_ncia',
  'Montante': 'Montante',
};

const INTERMEDIACAO_COLUMNS = {
  ...SP_PIPELINE_COLUMNS,
  'Ano': 'Ano',
  'Mês': 'M_x00ea_s',
  'Nº Semana': 'N_x00ba_semana',
};

const ANGARIACAO_COLUMNS = {
  'Operação': 'Opera_x00e7__x00e3_o',
  'Consultor': 'Consultor',
  'Estado': 'Estado',
  'Prob.': 'ProbabilidadedeConcretiza_x00e7__x00e3_o',
  'Natureza': 'Naturezadeneg_x00f3_cio',
  'Comissão': 'Comiss_x00e3_oTotalHABITA',
  'Data': 'Data',
  'Faturação': 'DataPrevista_x002f_EfetivaConclu',
  'Recebimento': 'Dataprevistaderecebimento',
  'Referência': 'Refer_x00ea_ncia',
  'Montante': 'Montante',
  'Ano': 'Ano',
  'Mês': 'M_x00ea_s',
  'Nº Semana': 'N_x00ba_semana',
};

const SOI_OBJECTIVE_FIELDS = [
  { key: 'faturacao', label: 'Faturação', money: true },
  { key: 'angariacao', label: 'Angariação', money: false },
  { key: 'oportunidades', label: 'Oportunidades', money: false },
  { key: 'transacoes', label: 'Transações', money: false },
  { key: 'intermediacao', label: 'Intermediação', money: false },
];

function soiTone(score) {
  if (score == null || score === '') return 'idle';
  const n = Number(score);
  if (Number.isNaN(n)) return 'idle';
  if (n < 100) return 'low';
  if (n <= 150) return 'warn';
  return 'good';
}

function formatSoiAmount(value, money) {
  if (value == null || value === '') return '—';
  return money ? formatCurrency(value) : formatNumber(value);
}

function formatSoiGro(gro) {
  if (gro == null || gro === '') return '—';
  const n = Number(gro);
  if (Number.isNaN(n)) return '—';
  return `${new Intl.NumberFormat('pt-PT', { maximumFractionDigits: 1 }).format(n)}%`;
}

function blankSoiObjectives() {
  return { faturacao: '', angariacao: '', oportunidades: '', transacoes: '', intermediacao: '' };
}

function soiObjectivesFromQuarter(quarter) {
  const values = blankSoiObjectives();
  const indicators = quarter?.indicators || quarter?.Indicators || [];
  indicators.forEach(ind => {
    const key = ind.key || ind.Key;
    if (!Object.prototype.hasOwnProperty.call(values, key)) return;
    const obj = ind.objetivo ?? ind.Objetivo;
    values[key] = obj == null || obj === '' ? '' : String(obj);
  });
  return values;
}

function parseSoiObjective(value) {
  if (value == null || String(value).trim() === '') return null;
  const n = parseNumericValue(value);
  return Number.isNaN(n) ? null : n;
}

const COMISSOES_COLUMNS = {
  'Consultor': 'Consultor',
  'Valor': 'valor',
  'Comissão Consultor': 'valor_consultor',
  'Comissão HABITA': 'valor_habita',
  '% Partilha': 'perc_partilha',
  'Validação': 'validacao',
  'Liquidado': 'liquidado',
  'Data': 'Data',
  'Ano': 'Ano_x0020_de_x0020_Cria_x00e7__x',
};

const PEDIDOS_FATURACAO_GRID = [
  { key: 'Consultor', label: 'Consultor', visible: true, order: 0, width: 140 },
  { key: 'ref_imovel', label: 'Imóvel', visible: true, order: 1, displayPath: 'value', width: 108 },
  { key: 'tipo_negocio', label: 'Tipo neg.', visible: true, order: 2, width: 96 },
  { key: 'local', label: 'Local', visible: true, order: 3, width: 96 },
  { key: 'valor_comissao', label: 'Comissão', visible: true, order: 4, width: 112 },
  { key: 'validado', label: 'Validado', visible: true, order: 5, width: 96 },
  { key: 'data_contrato', label: 'Data', visible: true, order: 6, width: 104 },
  { key: 'Ano_x0020_de_x0020_Cria_x00e7__x', label: 'Ano', visible: true, order: 7, width: 64 },
  { key: 'tipo', label: 'Tipo', visible: false, order: 8, width: 88 },
  { key: 'concluido', label: 'Concluído', visible: false, order: 9, width: 88 },
  { key: 'data_prevista', label: 'Data prevista', visible: false, order: 10, width: 104 },
];

const FATURACAO_DOSSIER_COMISSOES_GRID = [
  { key: 'Consultor', label: 'Consultor', visible: true, order: 0, width: 120 },
  { key: 'valor', label: 'Valor', visible: true, order: 1, width: 100 },
  { key: 'valor_consultor', label: 'Com. consultor', visible: true, order: 2, width: 108 },
  { key: 'valor_habita', label: 'Com. HABITA', visible: true, order: 3, width: 108 },
  { key: 'validacao', label: 'Validação', visible: true, order: 4, width: 96 },
  { key: 'liquidado', label: 'Liquidado', visible: true, order: 5, width: 88 },
  { key: 'Data', label: 'Data', visible: true, order: 6, width: 100 },
];

const FATURACAO_DOSSIER_LINES_GRID = [
  { key: 'Entidade', label: 'Entidade', visible: true, order: 0, width: 140 },
  { key: 'EntidadeNif', label: 'NIF', visible: true, order: 1, width: 100 },
  { key: 'valor', label: 'Valor', visible: true, order: 2, width: 100 },
  { key: 'estado', label: 'Estado', visible: true, order: 3, width: 96 },
  { key: 'faturado', label: 'Faturado', visible: true, order: 4, width: 88 },
  { key: 'liquidado', label: 'Liquidado', visible: true, order: 5, width: 88 },
  { key: 'Data', label: 'Data', visible: true, order: 6, width: 100 },
];

const PEDIDOS_COLUMNS = {
  'Consultor': 'Consultor',
  'Imóvel': 'ref_imovel',
  'Tipo': 'tipo',
  'Tipo negócio': 'tipo_negocio',
  'Local': 'local',
  'Valor comissão': 'valor_comissao',
  'Validado': 'validado',
  'Concluído': 'concluido',
  'Data': 'data_contrato',
  'Data prevista': 'data_prevista',
  'Ano': 'Ano_x0020_de_x0020_Cria_x00e7__x',
};

const FATURACAO_LINE_COLUMNS = {
  'Pedido': 'ID_pedido',
  'Entidade': 'Entidade',
  'NIF': 'EntidadeNif',
  'Valor': 'valor',
  '%': 'parcentagem',
  'Estado': 'estado',
  'Faturado': 'faturado',
  'Liquidado': 'liquidado',
  'Cond. pagamento': 'cond_pagamento',
  'Data': 'Data',
};

function MediacaoTabsBar({ mediacaoTab, onTabChange }) {
  return (
    <div className="tabs tabs-segmented module-tabs" role="tablist" aria-label="Secções Mediação">
      <button
        type="button"
        className={`tab ${mediacaoTab === 'negocios' ? 'active' : ''}`}
        role="tab"
        aria-selected={mediacaoTab === 'negocios'}
        onClick={() => onTabChange('negocios')}
      >
        Negócios
      </button>
      <button
        type="button"
        className={`tab ${mediacaoTab === 'faturacao' ? 'active' : ''}`}
        role="tab"
        aria-selected={mediacaoTab === 'faturacao'}
        onClick={() => onTabChange('faturacao')}
      >
        Faturação
      </button>
      <button
        type="button"
        className={`tab ${mediacaoTab === 'comissao' ? 'active' : ''}`}
        role="tab"
        aria-selected={mediacaoTab === 'comissao'}
        onClick={() => onTabChange('comissao')}
      >
        Comissão
      </button>
    </div>
  );
}

function fieldOr(obj, ...keys) {
  if (!obj) return null;
  for (const k of keys) {
    const v = getFieldValue(obj, k);
    if (v != null && v !== '') {
      if (typeof v === 'object') {
        const nested = v.lookupValue ?? v.value ?? v.Title ?? v.displayName;
        if (nested != null && nested !== '') return nested;
      }
      return v;
    }
  }
  return null;
}

function FaturacaoDossierPanel({ pedido, imovel, erpLines, faturacao, comissoes, loading, error, onClose, onCopyErp, phoneBack = false }) {
  if (loading) return <LoadingSpinner message="A carregar dossier…" />;
  if (error) return <ErrorBanner error={error} onDismiss={() => {}} />;
  if (!pedido) return null;

  const pedidoId = pedido.id || pedido.Id;
  const totalFaturacao = faturacao.reduce((sum, row) => {
    const v = getFieldValue(row, 'valor') ?? getFieldValue(row, 'Valor');
    const n = typeof v === 'number' ? v : parseFloat(String(v || '').replace(/\s/g, '').replace(',', '.'));
    return sum + (Number.isNaN(n) ? 0 : n);
  }, 0);

  return (
    <div className="faturacao-dossier-panel">
      <div className="faturacao-dossier-header">
        <div>
          {phoneBack && (
            <button type="button" className="btn btn-secondary btn-sm phone-back-btn" onClick={onClose} style={{ marginBottom: '0.5rem' }}>
              <Icon name="arrowLeft" size={14} />
              Voltar
            </button>
          )}
          <h3>Pedido #{pedidoId}</h3>
          <p className="faturacao-dossier-sub">
            {fieldOr(pedido, 'Consultor') || '—'} · {fieldOr(pedido, 'tipo_negocio', 'tipo') || '—'} · {fieldOr(pedido, 'local') || '—'}
          </p>
        </div>
        <div className="faturacao-dossier-actions">
          <button type="button" className="btn btn-primary btn-sm" onClick={onCopyErp} disabled={!erpLines.length}>
            Copiar para ERP
          </button>
          <button type="button" className="btn btn-secondary btn-sm" onClick={onClose}>
            {phoneBack ? 'Voltar' : 'Fechar'}
          </button>
        </div>
      </div>

      <div className="faturacao-kpi-row">
        <div className="faturacao-kpi-card">
          <span className="faturacao-kpi-label">Valor comissão</span>
          <strong>{formatCurrency(fieldOr(pedido, 'valor_comissao', 'ValorComissao'))}</strong>
        </div>
        <div className="faturacao-kpi-card">
          <span className="faturacao-kpi-label">Linhas faturação</span>
          <strong>{faturacao.length}</strong>
        </div>
        <div className="faturacao-kpi-card">
          <span className="faturacao-kpi-label">Total faturado</span>
          <strong>{formatCurrency(totalFaturacao)}</strong>
        </div>
        <div className="faturacao-kpi-card">
          <span className="faturacao-kpi-label">Comissões</span>
          <strong>{comissoes.length}</strong>
        </div>
      </div>

      <div className="faturacao-dossier-body">
        <div className="faturacao-dossier-row">
          <div className="faturacao-dossier-card">
            <h4>Pedido</h4>
            <dl className="faturacao-dl">
              <div><dt>Consultor</dt><dd>{fieldOr(pedido, 'Consultor') || '—'}</dd></div>
              <div><dt>Tipo</dt><dd>{fieldOr(pedido, 'tipo') || '—'}</dd></div>
              <div><dt>Tipo negócio</dt><dd>{fieldOr(pedido, 'tipo_negocio') || '—'}</dd></div>
              <div><dt>Local</dt><dd>{fieldOr(pedido, 'local') || '—'}</dd></div>
              <div><dt>Validado</dt><dd>{fieldOr(pedido, 'validado') || '—'}</dd></div>
              <div><dt>Concluído</dt><dd>{fieldOr(pedido, 'concluido') || '—'}</dd></div>
              <div><dt>Data contrato</dt><dd>{formatDate(fieldOr(pedido, 'data_contrato', 'Data'))}</dd></div>
              <div><dt>Data prevista</dt><dd>{formatDate(fieldOr(pedido, 'data_prevista'))}</dd></div>
              <div><dt>Ano</dt><dd>{fieldOr(pedido, 'Ano_x0020_de_x0020_Cria_x00e7__x', 'Ano') || '—'}</dd></div>
            </dl>
          </div>
          <div className="faturacao-dossier-card">
            <h4>Imóvel (lookup)</h4>
            <dl className="faturacao-dl">
              <div><dt>Referência</dt><dd>{fieldOr(imovel, 'Title', 'Refer_x00ea_ncia') || fieldOr(pedido, 'ImovelReferencia', 'ref_imovel') || '—'}</dd></div>
              <div><dt>Morada</dt><dd>{fieldOr(imovel, 'morada', 'Morada') || fieldOr(pedido, 'ImovelMorada') || '—'}</dd></div>
              <div><dt>Valor</dt><dd>{formatCurrency(fieldOr(imovel, 'valor', 'Valor') ?? fieldOr(pedido, 'ImovelValor'))}</dd></div>
            </dl>
          </div>
        </div>
        <div className="faturacao-dossier-card faturacao-erp-card">
          <h4>Resumo ERP — entidades para faturação</h4>
          {erpLines.length === 0 ? (
            <p className="empty-hint">Sem linhas de faturação ligadas a este pedido.</p>
          ) : erpLines.map((l, i) => (
            <div key={l.faturacaoId || l.FaturacaoId || i} className="faturacao-erp-line">
              <div className="faturacao-erp-line-head">
                <strong>{l.entidadeNome || l.EntidadeNome || 'Entidade'}</strong>
                <span className={`status-badge ${statusClass(l.faturado || l.Faturado)}`}>
                  Faturado: {l.faturado || l.Faturado || '—'}
                </span>
              </div>
              <dl className="faturacao-erp-dl">
                <div><dt>NIF</dt><dd>{l.nif || l.Nif || '—'}</dd></div>
                <div><dt>Morada</dt><dd>{l.morada || l.Morada || '—'}</dd></div>
                <div><dt>Valor</dt><dd>{formatCurrency(l.valor ?? l.Valor)}</dd></div>
                <div><dt>Liquidado</dt><dd>{l.liquidado || l.Liquidado || '—'}</dd></div>
              </dl>
            </div>
          ))}
        </div>
        <div className="faturacao-dossier-card faturacao-dossier-card-wide">
          <h4>Linhas de faturação ({faturacao.length})</h4>
          <DataGrid
            storageKey="faturacao-dossier-lines"
            columns={FATURACAO_DOSSIER_LINES_GRID}
            rows={faturacao}
            emptyMessage="Sem linhas."
            showColumnChooser={false}
          />
        </div>
        <div className="faturacao-dossier-card faturacao-dossier-card-wide">
          <h4>Comissões ({comissoes.length})</h4>
          <DataGrid
            storageKey="faturacao-dossier-comissoes"
            columns={FATURACAO_DOSSIER_COMISSOES_GRID}
            rows={comissoes}
            emptyMessage="Sem comissões."
            showColumnChooser={false}
          />
        </div>
      </div>
    </div>
  );
}

function FaturacaoPage({ user, headerSlot, onConfigureListColumns }) {
  const [selectedPedido, setSelectedPedido] = useState(null);
  const [dossier, setDossier] = useState(null);
  const [dossierLoading, setDossierLoading] = useState(false);
  const [dossierError, setDossierError] = useState(null);
  const viewportMode = useViewportMode();
  const isPhone = viewportMode === 'phone';
  const showDetail = isPhone && !!selectedPedido;

  useEffect(() => {
    if (!selectedPedido) {
      setDossier(null);
      return;
    }
    const id = selectedPedido.id || selectedPedido.Id;
    let cancelled = false;
    setDossierLoading(true);
    setDossierError(null);
    apiFetch(`/api/faturacao/pedidos/${encodeURIComponent(id)}/dossier`)
      .then(res => { if (!cancelled) setDossier(res); })
      .catch(err => { if (!cancelled) setDossierError({ message: err.message, code: err.code }); })
      .finally(() => { if (!cancelled) setDossierLoading(false); });
    return () => { cancelled = true; };
  }, [selectedPedido]);

  const pedido = dossier?.pedido || dossier?.Pedido || selectedPedido;
  const imovel = dossier?.imovel || dossier?.Imovel;
  const erpLines = dossier?.erpLines || dossier?.ErpLines || [];
  const comissoes = dossier?.comissoes || dossier?.Comissoes || [];
  const faturacao = dossier?.faturacao || dossier?.Faturacao || [];

  const copyErpSummary = async () => {
    const lines = erpLines.map((l, i) => {
      const nome = l.entidadeNome ?? l.EntidadeNome ?? '—';
      const nif = l.nif ?? l.Nif ?? '—';
      const morada = l.morada ?? l.Morada ?? '—';
      const valor = formatCurrency(l.valor ?? l.Valor);
      const pct = l.percentagem ?? l.Percentagem;
      const pctText = pct == null ? '—' : `${Math.round((pct <= 1 ? pct * 100 : pct) * 100) / 100}%`;
      return [
        `Linha ${i + 1}`,
        `Entidade: ${nome}`,
        `NIF: ${nif}`,
        `Morada: ${morada}`,
        `Valor: ${valor}`,
        `%: ${pctText}`,
        `Cond. pagamento: ${l.condPagamento ?? l.CondPagamento ?? '—'}`,
        `Estado: ${l.estado ?? l.Estado ?? '—'}`,
        `Faturado: ${l.faturado ?? l.Faturado ?? '—'}`,
        `Liquidado: ${l.liquidado ?? l.Liquidado ?? '—'}`,
      ].join('\n');
    });
    const refImovel = fieldOr(imovel, 'Title', 'Refer_x00ea_ncia') || '—';
    const text = [
      `Pedido #${pedido?.id || pedido?.Id || '—'}`,
      `Consultor: ${fieldOr(pedido, 'Consultor') || '—'}`,
      `Imóvel: ${refImovel}`,
      `Morada imóvel: ${fieldOr(imovel, 'morada') || '—'}`,
      `Valor comissão pedido: ${formatCurrency(fieldOr(pedido, 'valor_comissao'))}`,
      '',
      ...lines,
    ].join('\n');
    try {
      await navigator.clipboard.writeText(text);
      toast.success('Resumo ERP copiado.');
    } catch {
      prompt('Copiar resumo ERP:', text);
    }
  };

  return (
    <div className="page-body">
      <div className="page-content faturacao-page">
        <ModulePageShell tabs={headerSlot || null} className="module-page-shell-split">
          <div className={`faturacao-split${isPhone ? ' phone-master-detail' : ''}${showDetail ? ' phone-show-detail' : ''}`}>
            <div className="faturacao-pedidos-col">
              <GenericListPage
                key="pedidos-faturacao"
                user={user}
                listKey="pedidos"
                gridStorageKey="pedidos-faturacao"
                columns={PEDIDOS_FATURACAO_GRID}
                hideHeader
                embedded
                hideCreate
                enableEstadoFilter
                estadoFilterField="validado"
                selectedRowId={selectedPedido?.id || selectedPedido?.Id}
                onRowSelect={(row) => setSelectedPedido(row)}
                onConfigureListColumns={onConfigureListColumns}
              />
            </div>
            <aside className="faturacao-dossier-col" aria-label="Dossier financeiro">
              {!selectedPedido ? (
                <div className="faturacao-dossier-empty">
                  <Icon name="document" size={32} />
                  <h3>Dossier financeiro</h3>
                  <p>Selecione um pedido na grelha para ver imóvel, linhas de faturação, comissões e dados para o ERP.</p>
                </div>
              ) : (
                <FaturacaoDossierPanel
                  pedido={pedido}
                  imovel={imovel}
                  erpLines={erpLines}
                  faturacao={faturacao}
                  comissoes={comissoes}
                  loading={dossierLoading}
                  error={dossierError}
                  onClose={() => setSelectedPedido(null)}
                  onCopyErp={copyErpSummary}
                  phoneBack={isPhone}
                />
              )}
            </aside>
          </div>
        </ModulePageShell>
      </div>
    </div>
  );
}

const CARTEIRA_TABS = [
  {
    id: 'matching',
    slug: 'matching',
    label: 'Matching',
    columns: null,
  },
  {
    id: 'ativos',
    slug: 'carteira',
    label: 'Ativos',
    columns: {
      'Identificação Ativo': 'Denomina_x00e7__x00e3_oAtivo',
      'Potencial de Ativo': 'Title',
      'Tipo de Ativo': 'TipoAtivoMatch',
      'Localização': 'LocalizacaoMatch',
      'Operação': 'Opera_x00e7__x00e3_o',
      'Área (m²)': 'AreaBrutaM2',
      'Valor (€)': 'ValorEuro',
      'Estado': 'Estado',
      'Consultor': 'Consultor',
    },
  },
  {
    id: 'oportunidades',
    slug: 'oportunidades',
    label: 'Oportunidades',
    columns: {
      'Identificação Oportunidade': 'Title',
      'Potencial': 'PotencialOportunidade',
      'Tipo de Ativo': 'TipoAtivoMatch',
      'Localização': 'LocalizacaoMatch',
      'Operação': 'Opera_x00e7__x00e3_o',
      'Valor min (€)': 'ValorMinEuro',
      'Estado': 'Estado',
      'Clientes': 'Clientes',
      'Consultor': 'Consultor',
    },
  },
  {
    id: 'clientes',
    slug: 'clientes',
    label: 'Clientes',
    columns: {
      'Identificação Cliente': 'Title',
      'Tipo de cliente': 'TipoCliente',
      'Email': 'Email',
      'Contacto': 'Contacto',
      'Consultor': 'Consultor',
    },
  },
];

const MATCHING_OPORTUNIDADE_GRID = [
  { key: 'Title', label: 'Oportunidade', visible: true, order: 0, width: 160 },
  { key: 'Clientes', label: 'Clientes', visible: true, order: 1, width: 140, displayPath: 'value' },
  { key: 'Opera_x00e7__x00e3_o', label: 'Operação', visible: true, order: 2, width: 96 },
  { key: 'LocalizacaoMatch', label: 'Localização', visible: true, order: 3, width: 110 },
  { key: 'TipoAtivoMatch', label: 'Tipo ativo', visible: true, order: 4, width: 110 },
  { key: 'PotencialOportunidade', label: 'Potencial', visible: true, order: 5, width: 110 },
  { key: 'Estado', label: 'Estado', visible: true, order: 6, width: 88 },
  { key: 'ValorMinEuro', label: 'Valor min', visible: true, order: 7, width: 100, format: 'money' },
  { key: 'Consultor', label: 'Consultor', visible: true, order: 8, width: 130 },
];

const MATCHING_ATIVO_RESULT_GRID = [
  { key: 'title', label: 'Ativo', visible: true, order: 0, width: 160 },
  { key: 'consultor', label: 'Consultor', visible: true, order: 1, width: 130 },
  { key: 'estado', label: 'Estado', visible: true, order: 2, width: 88 },
  { key: 'operacao', label: 'Operação', visible: true, order: 3, width: 96 },
  { key: 'localizacao', label: 'Localização', visible: true, order: 4, width: 110 },
  { key: 'tipos', label: 'Tipo ativo', visible: true, order: 5, width: 110 },
  { key: 'valor', label: 'Valor', visible: true, order: 6, width: 100, format: 'money' },
  { key: 'area', label: 'Área (m²)', visible: true, order: 7, width: 90, format: 'number' },
  { key: 'score', label: 'Match', visible: true, order: 8, width: 72 },
];

const MATCH_CRITERIA = [
  { id: 'operacao', label: 'Operação' },
  { id: 'tipoAtivo', label: 'Tipo de ativo' },
  { id: 'localizacao', label: 'Localização' },
  { id: 'potencial', label: 'Potencial' },
  { id: 'area', label: 'Área' },
  { id: 'valor', label: 'Valor' },
  { id: 'rendimento', label: 'Rendimento' },
];

function normMatchText(value) {
  return String(value ?? '')
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .toLowerCase()
    .replace(/\s+/g, ' ')
    .trim();
}

function displayMatchValue(item, ...keys) {
  for (const key of keys) {
    if (!key) continue;
    const v = getLookupDisplayValue(item, key) ?? getFieldValue(item, key);
    if (v == null || v === '') continue;
    if (Array.isArray(v)) {
      const joined = invChoiceSet(v).join(', ');
      if (joined) return joined;
      continue;
    }
    const text = typeof v === 'object'
      ? (objectDisplayText(v) || v.lookupValue || v.value || v.Title || v.displayName || v.DisplayName || '')
      : String(v);
    if (String(text).trim()) return String(text).trim();
  }
  return '';
}

function valuesMatchLoose(a, b) {
  const na = normMatchText(a);
  const nb = normMatchText(b);
  if (!na || !nb) return false;
  if (na === nb) return true;
  if (na.includes(nb) || nb.includes(na)) return true;
  return false;
}

function invNum(item, ...keys) {
  for (const key of keys) {
    const v = getFieldValue(item, key);
    const n = invParseNumber(v);
    if (n != null) return n;
  }
  return null;
}

function invFieldSet(item, ...keys) {
  const out = [];
  keys.forEach((key) => out.push(...invChoiceSet(getFieldValue(item, key))));
  return [...new Set(out)];
}

function invOpenEndedBound(n) {
  if (n == null || n <= 0) return null;
  return n;
}

function extractInvProfile(item, kind) {
  const denom = displayMatchValue(item, 'Denomina_x00e7__x00e3_oAtivo');
  const rawTitle = displayMatchValue(item, 'Title', 'title');
  const title = kind === 'ativo'
    ? ((denom && !denom.startsWith('[')) ? denom : ((rawTitle && !rawTitle.startsWith('[')) ? rawTitle : (denom || 'Ativo')))
    : (rawTitle || 'Oportunidade');
  const potencialRaw = kind === 'ativo'
    ? invFieldSet(item, 'PotencialAtivo', 'Title')
    : invFieldSet(item, 'PotencialOportunidade', 'PotencialdaOportunidade');
  const potencial = potencialRaw.filter(p => INV_POTENCIAL.some(k => valuesMatchLoose(k, p)));
  return {
    title,
    operacao: invOperacao(item),
    tipos: invFieldSet(item, 'TipoAtivoMatch', 'TipodeAtivo', 'TipoAtivo'),
    localizacoes: invFieldSet(item, 'LocalizacaoMatch', 'Localiza_x00e7__x00e3_o'),
    potencial,
    area: kind === 'ativo' ? invNum(item, 'AreaBrutaM2', 'field_9') : null,
    areaMin: kind === 'ativo' ? null : invOpenEndedBound(invNum(item, 'AreaBrutaMin', 'field_10')),
    areaMax: kind === 'ativo' ? null : invOpenEndedBound(invNum(item, 'AreaBrutaMax', 'AreabrutaMax_x0028_m2_x0029_')),
    valor: kind === 'ativo' ? invNum(item, 'ValorEuro', 'field_8', 'Valor') : null,
    valorMin: kind === 'ativo' ? null : invOpenEndedBound(invNum(item, 'ValorMinEuro', 'field_8')),
    valorMax: kind === 'ativo' ? null : invOpenEndedBound(invNum(item, 'ValorMax_x0028__x20ac__x0029_')),
    rendimento: kind === 'ativo' ? invNum(item, 'Rendimento') : null,
    rendimentoMin: kind === 'ativo' ? null : invOpenEndedBound(invNum(item, 'RendimentoMinPct', 'RendimentoMin_x0028__x0025__x002')),
    estado: displayMatchValue(item, 'Estado', 'estado'),
    clientes: displayMatchValue(item, 'Clientes', 'cliente'),
    consultor: displayMatchValue(item, 'ConsultorResponsavel', 'Consultor', 'consultor'),
    item,
  };
}

function invOperacao(item) {
  return displayMatchValue(item, 'Opera_x00e7__x00e3_o', 'Operacao', 'operacao', 'Operação');
}

function invSetsOverlap(a, b) {
  if (!a.length || !b.length) return false;
  const nb = new Set(b.map(normMatchText));
  return a.some(x => nb.has(normMatchText(x)));
}

function invRangeFits(point, min, max) {
  if (point == null) return true;
  if (min == null && max == null) return true;
  if (min != null && point < min) return false;
  if (max != null && point > max) return false;
  return true;
}

function ativoFitsProfile(ativoItem, oppItem, enabled = {}) {
  const a = extractInvProfile(ativoItem, 'ativo');
  const o = extractInvProfile(oppItem, 'opp');
  const reasons = [];
  let requiredFail = false;

  if (enabled.operacao !== false) {
    if (o.operacao && a.operacao && valuesMatchLoose(o.operacao, a.operacao)) reasons.push('Operação');
    else if (o.operacao && a.operacao) requiredFail = true;
  }
  if (enabled.tipoAtivo !== false) {
    if (invSetsOverlap(o.tipos, a.tipos)) reasons.push('Tipo de ativo');
    else if (o.tipos.length && a.tipos.length) requiredFail = true;
  }
  if (enabled.localizacao !== false) {
    if (invSetsOverlap(o.localizacoes, a.localizacoes)) reasons.push('Localização');
    else if (o.localizacoes.length && a.localizacoes.length) requiredFail = true;
  }
  if (enabled.potencial !== false && invSetsOverlap(o.potencial, a.potencial)) {
    reasons.push('Potencial');
  }
  if (enabled.area !== false && a.area != null && (o.areaMin != null || o.areaMax != null)
      && invRangeFits(a.area, o.areaMin, o.areaMax)) {
    reasons.push('Área');
  }
  if (enabled.valor !== false && a.valor != null && (o.valorMin != null || o.valorMax != null)
      && invRangeFits(a.valor, o.valorMin, o.valorMax)) {
    reasons.push('Valor');
  }
  if (enabled.rendimento !== false && a.rendimento != null && o.rendimentoMin != null
      && a.rendimento >= o.rendimentoMin) {
    reasons.push('Rendimento');
  }

  if (requiredFail || reasons.length === 0) {
    return { score: 0, reasons: [], ...a, ativo: ativoItem };
  }

  const estadoNorm = normMatchText(a.estado);
  if (estadoNorm.includes('dispon')) reasons.push('Disponível');

  const core = ['Operação', 'Tipo de ativo', 'Localização'].filter(r => reasons.includes(r)).length;
  const extra = reasons.length - core;
  const score = Math.min(100, core * 28 + extra * 8 + (reasons.includes('Disponível') ? 8 : 0));
  return { score, reasons, ...a, ativo: ativoItem };
}

function bestAtivoMatchScore(oppItem, ativos, criteria) {
  if (!oppItem || !ativos?.length) return 0;
  let best = 0;
  for (let i = 0; i < ativos.length; i += 1) {
    const score = ativoFitsProfile(ativos[i], oppItem, criteria).score;
    if (score > best) best = score;
    if (best >= 100) return 100;
  }
  return best;
}

function profilesOverlap(aItem, bItem) {
  const a = extractInvProfile(aItem, 'opp');
  const b = extractInvProfile(bItem, 'opp');
  if (!a.operacao || !b.operacao) return { overlap: false, reason: 'operacao-empty' };
  if (!valuesMatchLoose(a.operacao, b.operacao)) return { overlap: false, reason: 'operacao' };
  if (!a.tipos.length || !b.tipos.length) return { overlap: false, reason: 'tipo-empty' };
  if (!invSetsOverlap(a.tipos, b.tipos)) return { overlap: false, reason: 'tipo' };
  if (!a.localizacoes.length || !b.localizacoes.length) return { overlap: false, reason: 'loc-empty' };
  if (!invSetsOverlap(a.localizacoes, b.localizacoes)) return { overlap: false, reason: 'loc' };
  return { overlap: true, a, b };
}

function profileFromWizardForm(form) {
  return {
    Title: form.title || '',
    Opera_x00e7__x00e3_o: form.operacaoLabel || form.operacao || '',
    TipoAtivo: form.tipos?.[0] || '',
    TipoAtivoMatch: form.tipos || [],
    Localiza_x00e7__x00e3_o: form.localizacoes?.[0] || '',
    LocalizacaoMatch: form.localizacoes || [],
    PotencialdaOportunidade: form.potencial || [],
    PotencialOportunidade: form.potencial || [],
    field_10: Number(form.areaMin) > INV_SLIDER_AREA.min ? Number(form.areaMin) : null,
    AreaBrutaMin: Number(form.areaMin) > INV_SLIDER_AREA.min ? Number(form.areaMin) : null,
    AreabrutaMax_x0028_m2_x0029_: Number(form.areaMax) !== INV_SLIDER_AREA.max ? Number(form.areaMax) : null,
    AreaBrutaMax: Number(form.areaMax) !== INV_SLIDER_AREA.max ? Number(form.areaMax) : null,
    field_8: Number(form.valorMin) > INV_SLIDER_VALOR.min ? Number(form.valorMin) : null,
    ValorMinEuro: Number(form.valorMin) > INV_SLIDER_VALOR.min ? Number(form.valorMin) : null,
    ValorMax_x0028__x20ac__x0029_: Number(form.valorMax) !== INV_SLIDER_VALOR.max ? Number(form.valorMax) : null,
    RendimentoMin_x0028__x0025__x002: Number(form.rendimentoMin) > INV_SLIDER_REND.min ? Number(form.rendimentoMin) : null,
    RendimentoMinPct: Number(form.rendimentoMin) > INV_SLIDER_REND.min ? Number(form.rendimentoMin) : null,
    Estado: form.estado || 'Ativa',
  };
}

async function fetchListItemsPages(listKey, { pageSize = 100, maxPages = 8 } = {}) {
  const all = [];
  let token = null;
  for (let i = 0; i < maxPages; i++) {
    const params = new URLSearchParams({ pageSize: String(pageSize) });
    if (token) params.set('pageToken', token);
    const result = await apiFetch(`/api/lists/${encodeURIComponent(listKey)}/items?${params}`);
    const items = result.items || result.Items || [];
    all.push(...items);
    token = result.nextPageToken ?? result.NextPageToken ?? null;
    if (!token || items.length === 0) break;
  }
  return all;
}

function matchingCardFields(m) {
  const fields = [];
  if (m.consultor) fields.push({ id: 'consultor', label: 'Consultor', value: m.consultor });
  if (m.estado) fields.push({ id: 'estado', label: 'Estado', value: m.estado });
  if (m.operacao) fields.push({ id: 'operacao', label: 'Operação', value: m.operacao });
  if (m.tipos?.length) fields.push({ id: 'tipo', label: 'Tipo', value: m.tipos.join(', ') });
  if (m.localizacoes?.length) fields.push({ id: 'loc', label: 'Localização', value: m.localizacoes.join(', ') });
  if (m.potencial?.length) fields.push({ id: 'pot', label: 'Potencial', value: m.potencial.join(', ') });
  if (m.valor != null) fields.push({ id: 'valor', label: 'Valor', value: formatCurrency(m.valor) });
  if (m.area != null) fields.push({ id: 'area', label: 'Área', value: `${m.area} m²` });
  if (m.valorMin != null) fields.push({ id: 'valorMin', label: 'Valor min', value: formatCurrency(m.valorMin) });
  if (m.rendimento != null) fields.push({ id: 'rendimento', label: 'Rendimento', value: formatPercent(m.rendimento) });
  if (m.rendimentoMin != null) fields.push({ id: 'rendimentoMin', label: 'Rendimento min', value: formatPercent(m.rendimentoMin) });
  return fields;
}

function CarteiraMatchingPanel({
  oportunidade,
  matches,
  loading,
  error,
  criteria,
  onCriteriaChange,
  onClose,
  phoneBack = false,
}) {
  const [listView, setListView] = useListLayoutView('matching-ativos', 'cards');
  const useCards = listView === 'cards';
  const profile = extractInvProfile(oportunidade, 'opp');
  const chips = [
    profile.consultor && { id: 'cons', label: 'Consultor', value: profile.consultor },
    profile.operacao && { id: 'op', label: 'Operação', value: profile.operacao },
    profile.tipos.length && { id: 'tipo', label: 'Tipo', value: profile.tipos.join(', ') },
    profile.localizacoes.length && { id: 'loc', label: 'Localização', value: profile.localizacoes.join(', ') },
    profile.potencial.length && { id: 'pot', label: 'Potencial', value: profile.potencial.join(', ') },
  ].filter(Boolean);
  const matchRows = matches.map(m => ({
    id: m.ativo?.id || m.ativo?.Id,
    title: m.title,
    consultor: m.consultor,
    estado: m.estado,
    operacao: m.operacao,
    localizacao: (m.localizacoes || []).join(', '),
    tipos: (m.tipos || []).join(', '),
    valor: m.valor,
    area: m.area,
    score: m.score != null ? `${m.score}%` : '',
  }));

  return (
    <div className="matching-panel">
      <div className="matching-panel-header">
        <div>
          {phoneBack && (
            <button type="button" className="btn btn-secondary btn-sm phone-back-btn" onClick={onClose} style={{ marginBottom: '0.5rem' }}>
              <Icon name="arrowLeft" size={14} />
              Voltar
            </button>
          )}
          <h3>{profile.title}</h3>
          <p className="matching-panel-sub">
            {profile.consultor ? `Consultor: ${profile.consultor}` : 'Sem consultor'}
            {profile.clientes ? ` · Clientes: ${profile.clientes}` : ''}
          </p>
        </div>
        <div className="matching-panel-header-actions">
          <PhoneListViewToggle view={listView} onChange={setListView} />
          <button type="button" className="btn btn-secondary btn-sm" onClick={onClose}>
            {phoneBack ? 'Voltar' : 'Limpar'}
          </button>
        </div>
      </div>

      {chips.length > 0 ? (
        <div className="matching-chips" aria-label="Perfil da oportunidade">
          {chips.map(c => (
            <span key={c.id} className="matching-chip">{c.label}: {c.value}</span>
          ))}
        </div>
      ) : (
        <p className="matching-panel-sub" style={{ padding: '0.65rem 1.1rem 0' }}>
          Esta oportunidade ainda não tem operação, tipo ou localização nos campos de matching.
        </p>
      )}

      <div className="matching-criteria" role="group" aria-label="Critérios de matching">
        {MATCH_CRITERIA.map(c => (
          <label key={c.id} className="matching-criteria-item">
            <input
              type="checkbox"
              checked={criteria[c.id] !== false}
              onChange={e => onCriteriaChange({ ...criteria, [c.id]: e.target.checked })}
            />
            <span>{c.label}</span>
          </label>
        ))}
      </div>

      <div className={`matching-panel-body${useCards ? '' : ' matching-panel-body-table'}`}>
        {loading && <LoadingSpinner message="A cruzar ativos…" />}
        {error && <ErrorBanner error={error} onDismiss={() => {}} />}
        {!loading && !error && matches.length === 0 && (
          <div className="matching-empty-inline">
            <Icon name="search" size={22} />
            <p>Nenhum ativo compatível com os critérios ativos.</p>
          </div>
        )}
        {!loading && matches.length > 0 && useCards && matches.map((m) => {
          const fields = matchingCardFields(m);
          return (
            <article key={m.ativo.id || m.ativo.Id || m.title} className="matching-card">
              <div className="matching-card-top">
                <strong className="matching-card-title">{m.title}</strong>
                <span className={`matching-score matching-score-${m.score >= 70 ? 'high' : m.score >= 40 ? 'mid' : 'low'}`}>
                  {m.score}%
                </span>
              </div>
              {fields.length > 0 && (
                <dl className="matching-card-fields">
                  {fields.map(f => (
                    <div key={f.id} className="matching-card-field">
                      <dt>{f.label}</dt>
                      <dd>{f.value}</dd>
                    </div>
                  ))}
                </dl>
              )}
              {m.reasons.length > 0 && (
                <div className="matching-reasons">
                  {m.reasons.map(r => <span key={r} className="matching-reason">{r}</span>)}
                </div>
              )}
            </article>
          );
        })}
        {!loading && matches.length > 0 && !useCards && (
          <DataGrid
            storageKey="matching-ativos"
            columns={MATCHING_ATIVO_RESULT_GRID}
            rows={matchRows}
            emptyMessage="Nenhum ativo compatível."
            showColumnChooser={false}
            preferSourceOrder
          />
        )}
      </div>
    </div>
  );
}

function CarteiraMatchingPage({ user, headerSlot, onConfigureListColumns, embedded = false }) {
  const [selectedOpp, setSelectedOpp] = useState(null);
  const [ativos, setAtivos] = useState([]);
  const [ativosLoading, setAtivosLoading] = useState(false);
  const [ativosError, setAtivosError] = useState(null);
  const [criteria, setCriteria] = useState({
    operacao: true,
    tipoAtivo: true,
    localizacao: true,
    potencial: true,
    area: true,
    valor: true,
    rendimento: true,
  });
  const viewportMode = useViewportMode();
  const isPhone = viewportMode === 'phone';
  const showDetail = isPhone && !!selectedOpp;

  useEffect(() => {
    let cancelled = false;
    (async () => {
      setAtivosLoading(true);
      setAtivosError(null);
      try {
        const items = await fetchListItemsPages('ativos', { pageSize: 100, maxPages: 10 });
        if (!cancelled) setAtivos(items);
      } catch (err) {
        if (!cancelled) setAtivosError({ message: err.message, code: err.code });
      } finally {
        if (!cancelled) setAtivosLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, []);

  const matches = useMemo(() => {
    if (!selectedOpp || !ativos.length) return [];
    return ativos
      .map(ativo => ativoFitsProfile(ativo, selectedOpp, criteria))
      .filter(m => m.score > 0)
      .sort((a, b) => b.score - a.score || a.title.localeCompare(b.title, 'pt'));
  }, [selectedOpp, ativos, criteria]);

  const body = (
    <MatchingSplitShell
      storageKey="matching"
      defaultLeftPct={30}
      className={`${embedded ? 'matching-split-embedded' : ''}${isPhone ? ' phone-master-detail' : ''}${showDetail ? ' phone-show-detail' : ''}`.trim()}
    >
      <div className="matching-oportunidades-col">
        <GenericListPage
          key="matching-oportunidades"
          user={user}
          listKey="oportunidades"
          gridStorageKey="matching-oportunidades"
          columns={MATCHING_OPORTUNIDADE_GRID}
          hideHeader
          embedded
          hideCreate
          selectedRowId={selectedOpp?.id || selectedOpp?.Id}
          onRowSelect={(row) => setSelectedOpp(row)}
          onConfigureListColumns={onConfigureListColumns}
          defaultListView="cards"
          matchAtivos={ativos}
          matchCriteria={criteria}
        />
      </div>
      <aside className="matching-ativos-col" aria-label="Ativos compatíveis">
        {!selectedOpp ? (
          <div className="matching-empty">
            <Icon name="search" size={32} />
            <h3>Matching de ativos</h3>
            <p>Selecione uma oportunidade à esquerda para ver ativos compatíveis (operação, tipo, localização e intervalos).</p>
          </div>
        ) : (
          <CarteiraMatchingPanel
            oportunidade={selectedOpp}
            matches={matches}
            loading={ativosLoading}
            error={ativosError}
            criteria={criteria}
            onCriteriaChange={setCriteria}
            onClose={() => setSelectedOpp(null)}
            phoneBack={isPhone}
          />
        )}
      </aside>
    </MatchingSplitShell>
  );

  if (embedded) return body;

  return (
    <div className="page-body">
      <div className="page-content matching-page">
        {headerSlot}
        {body}
      </div>
    </div>
  );
}

function InvestimentosClientesPage({ user, onConfigureListColumns }) {
  const [selected, setSelected] = useState(null);
  const [opps, setOpps] = useState([]);
  const [loading, setLoading] = useState(false);
  const viewportMode = useViewportMode();
  const isPhone = viewportMode === 'phone';
  const showDetail = isPhone && !!selected;

  useEffect(() => {
    let cancelled = false;
    (async () => {
      setLoading(true);
      try {
        const items = await fetchListItemsPages('oportunidades', { pageSize: 100, maxPages: 10 });
        if (!cancelled) setOpps(items);
      } catch {
        if (!cancelled) setOpps([]);
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, []);

  const clientId = selected ? String(selected.id || selected.Id || '') : '';
  const signed = useMemo(() => {
    if (!clientId) return [];
    return opps.filter(opp => invLookupIds(getFieldValue(opp, 'Clientes')).includes(clientId));
  }, [opps, clientId]);

  return (
    <MatchingSplitShell
      storageKey="clientes"
      defaultLeftPct={30}
      className={`matching-split-embedded${isPhone ? ' phone-master-detail' : ''}${showDetail ? ' phone-show-detail' : ''}`}
    >
      <div className="matching-oportunidades-col">
        <GenericListPage
          key="inv-clientes-list"
          user={user}
          listKey="clientes"
          gridStorageKey="inv-clientes"
          hideHeader
          embedded
          selectedRowId={selected?.id || selected?.Id}
          onRowSelect={(row) => setSelected(row)}
          onConfigureListColumns={onConfigureListColumns}
          defaultListView="cards"
        />
      </div>
      <aside className="matching-ativos-col" aria-label="Oportunidades do cliente">
        {!selected ? (
          <div className="matching-empty">
            <Icon name="team" size={32} />
            <h3>Oportunidades do cliente</h3>
            <p>Selecione um cliente à esquerda para ver as oportunidades a que está associado.</p>
          </div>
        ) : (
          <div className="matching-panel">
            <div className="matching-panel-header">
              <div>
                {isPhone && (
                  <button type="button" className="btn btn-secondary btn-sm phone-back-btn" onClick={() => setSelected(null)} style={{ marginBottom: '0.5rem' }}>
                    <Icon name="arrowLeft" size={14} />
                    Voltar
                  </button>
                )}
                <h3>{displayMatchValue(selected, 'Title', 'title') || 'Cliente'}</h3>
                <p className="matching-panel-sub">{signed.length} oportunidade{signed.length === 1 ? '' : 's'}</p>
              </div>
            </div>
            <div className="matching-panel-body">
              {loading && <LoadingSpinner message="A carregar oportunidades…" />}
              {!loading && signed.length === 0 && (
                <div className="matching-empty-inline">
                  <p>Este cliente ainda não está associado a nenhuma oportunidade.</p>
                </div>
              )}
              {signed.map((opp) => {
                const p = extractInvProfile(opp, 'opp');
                return (
                  <article key={opp.id || opp.Id} className="matching-card">
                    <div className="matching-card-top">
                      <strong className="matching-card-title">{p.title}</strong>
                      {p.estado && <span className="matching-chip">{p.estado}</span>}
                    </div>
                    <dl className="matching-card-fields">
                      {matchingCardFields(p).map(f => (
                        <div key={f.id} className="matching-card-field">
                          <dt>{f.label}</dt>
                          <dd>{f.value}</dd>
                        </div>
                      ))}
                    </dl>
                  </article>
                );
              })}
            </div>
          </div>
        )}
      </aside>
    </MatchingSplitShell>
  );
}

function NovaOportunidadeWizard({ user, open, onClose, onCreated }) {
  const [step, setStep] = useState(1);
  const [saving, setSaving] = useState(false);
  const [clientMode, setClientMode] = useState('existing');
  const [clients, setClients] = useState([]);
  const [opps, setOpps] = useState([]);
  const [locationChoices, setLocationChoices] = useState(INV_LOCALIZACAO_FALLBACK);
  const [oppCols, setOppCols] = useState([]);
  const [clientCols, setClientCols] = useState([]);
  const [duplicates, setDuplicates] = useState([]);
  const [checked, setChecked] = useState(false);
  const [form, setForm] = useState({
    clientId: '',
    clientTitle: '',
    tipoCliente: '',
    email: '',
    contacto: '',
    title: '',
    operacao: '',
    tipos: [],
    localizacoes: [],
    potencial: [],
    areaMin: INV_SLIDER_AREA.min,
    areaMax: INV_SLIDER_AREA.max,
    valorMin: INV_SLIDER_VALOR.min,
    valorMax: INV_SLIDER_VALOR.max,
    rendimentoMin: INV_SLIDER_REND.min,
    rendimentoMax: INV_SLIDER_REND.max,
    estado: 'Ativa',
  });

  useEffect(() => {
    if (!open) return;
    setStep(1);
    setDuplicates([]);
    setChecked(false);
    setClientMode('existing');
    setForm({
      clientId: '',
      clientTitle: '',
      tipoCliente: '',
      email: '',
      contacto: '',
      title: '',
      operacao: '',
      tipos: [],
      localizacoes: [],
      potencial: [],
      areaMin: INV_SLIDER_AREA.min,
      areaMax: INV_SLIDER_AREA.max,
      valorMin: INV_SLIDER_VALOR.min,
      valorMax: INV_SLIDER_VALOR.max,
      rendimentoMin: INV_SLIDER_REND.min,
      rendimentoMax: INV_SLIDER_REND.max,
      estado: 'Ativa',
    });
    let cancelled = false;
    (async () => {
      const [cli, opp, oppColRes, cliColRes] = await Promise.all([
        fetchListItemsPages('clientes', { pageSize: 100, maxPages: 8 }).catch(() => []),
        fetchListItemsPages('oportunidades', { pageSize: 100, maxPages: 10 }).catch(() => []),
        apiFetch('/api/settings/lists/by-key/oportunidades/columns').catch(() => []),
        apiFetch('/api/settings/lists/by-key/clientes/columns').catch(() => []),
      ]);
      if (cancelled) return;
      setClients(cli);
      setOpps(opp);
      const oppColumns = asSpColumnList(oppColRes);
      const cliColumns = asSpColumnList(cliColRes);
      setOppCols(oppColumns);
      setClientCols(cliColumns);
      const locCol = oppColumns.find(c => {
        const n = String(c.name || c.Name || '');
        return n === 'LocalizacaoMatch' || n === 'Localiza_x00e7__x00e3_o';
      });
      const choices = locCol?.choices || locCol?.Choices;
      if (Array.isArray(choices) && choices.length) setLocationChoices(choices.map(String));
    })();
    return () => { cancelled = true; };
  }, [open, user]);

  if (!open) return null;

  const selectedClient = clients.find(c => String(c.id || c.Id) === String(form.clientId));
  const clientName = clientMode === 'existing'
    ? (displayMatchValue(selectedClient, 'Title', 'title') || '')
    : form.clientTitle.trim();

  const canStep1 = clientMode === 'existing' ? !!form.clientId : form.clientTitle.trim().length > 1;
  const canStep2 = !!form.operacao && form.tipos.length > 0 && form.localizacoes.length > 0;

  const runDuplicateCheck = () => {
    const draft = { fields: profileFromWizardForm(form) };
    const hits = opps.filter(existing => profilesOverlap(draft, existing).overlap);
    setDuplicates(hits);
    setChecked(true);
    setStep(3);
  };

  const ensureClient = async () => {
    if (clientMode === 'existing' && form.clientId) return String(form.clientId);
    const created = await apiFetch('/api/lists/clientes/items', {
      method: 'POST',
      body: JSON.stringify({
        fields: applyInvColumnAliases('clientes', {
          Title: form.clientTitle.trim(),
          Email: String(form.email || '').trim() || undefined,
          Contacto: digitsOnly(form.contacto) || undefined,
          TipoCliente: form.tipoCliente || undefined,
        }, clientCols),
      }),
    });
    return String(created.id || created.Id);
  };

  const oppPayload = (clientId) => {
    const title = form.title.trim()
      || [clientName, form.operacao, form.tipos[0]].filter(Boolean).join(' · ');
    const fields = {
      Title: title,
      Opera_x00e7__x00e3_o: form.operacao,
      Estado: form.estado || 'Ativa',
      TipoAtivoMatch: form.tipos,
      LocalizacaoMatch: form.localizacoes,
    };
    const clientLookup = parseInt(clientId, 10);
    if (!Number.isNaN(clientLookup)) fields.ClientesLookupId = [clientLookup];
    if (form.potencial.length) fields.PotencialOportunidade = form.potencial;
    if (Number(form.areaMin) > INV_SLIDER_AREA.min) fields.AreaBrutaMin = Number(form.areaMin);
    if (Number(form.areaMax) !== INV_SLIDER_AREA.max) fields.AreaBrutaMax = Number(form.areaMax);
    if (Number(form.valorMin) > INV_SLIDER_VALOR.min) fields.ValorMinEuro = Number(form.valorMin);
    if (Number(form.valorMax) !== INV_SLIDER_VALOR.max) fields.ValorMaxEuro = Number(form.valorMax);
    if (Number(form.rendimentoMin) > INV_SLIDER_REND.min) fields.RendimentoMinPct = Number(form.rendimentoMin);
    return applyInvColumnAliases('oportunidades', fields, oppCols);
  };

  const signExisting = async (opp) => {
    setSaving(true);
    try {
      const clientId = await ensureClient();
      const existing = invLookupIds(getFieldValue(opp, 'Clientes'));
      const next = [...new Set([...existing, clientId])].map(n => parseInt(n, 10)).filter(n => !Number.isNaN(n));
      await apiFetch(`/api/lists/oportunidades/items/${opp.id || opp.Id}`, {
        method: 'PATCH',
        body: JSON.stringify({ fields: { ClientesLookupId: next } }),
      });
      toast.success('Cliente associado à oportunidade existente.');
      onCreated?.();
      onClose();
    } catch (err) {
      toast.error(err.message || 'Não foi possível associar o cliente.');
    } finally {
      setSaving(false);
    }
  };

  const createNew = async () => {
    if (!checked) {
      toast.warning('Verifique primeiro se já existe uma oportunidade com o mesmo perfil.');
      return;
    }
    setSaving(true);
    try {
      const clientId = await ensureClient();
      await apiFetch('/api/lists/oportunidades/items', {
        method: 'POST',
        body: JSON.stringify({ fields: oppPayload(clientId) }),
      });
      toast.success('Oportunidade criada.');
      onCreated?.();
      onClose();
    } catch (err) {
      toast.error(err.message || 'Não foi possível criar a oportunidade.');
    } finally {
      setSaving(false);
    }
  };

  return (
    <NegocioModalOverlay open={open} onClose={() => { if (!saving) onClose(); }} label="Nova oportunidade">
      <div className="negocio-modal-panel inv-wizard">
        <div className="negocio-modal-header">
          <div>
            <h3>Nova oportunidade</h3>
            <p className="negocio-modal-subtitle">
              {step === 1 && 'O cliente que pede o perfil'}
              {step === 2 && 'Perfil pedido (operação, tipo e localização obrigatórios)'}
              {step === 3 && 'Cruzamento com oportunidades existentes'}
            </p>
          </div>
          <button type="button" className="negocio-modal-close" onClick={onClose} aria-label="Fechar">
            <Icon name="close" size={18} />
          </button>
        </div>
        <div className="negocio-modal-body">
          {step === 1 && (
            <>
              <div className="segmented-control" role="radiogroup" aria-label="Cliente">
                <button type="button" className={`segmented-option${clientMode === 'existing' ? ' active' : ''}`} onClick={() => setClientMode('existing')}>Cliente existente</button>
                <button type="button" className={`segmented-option${clientMode === 'new' ? ' active' : ''}`} onClick={() => setClientMode('new')}>Novo cliente</button>
              </div>
              {clientMode === 'existing' ? (
                <div className="form-group">
                  <label>Cliente</label>
                  <AppSelect
                    value={form.clientId}
                    emptyOption="—"
                    options={clients.map(c => {
                      const id = String(c.id || c.Id);
                      return { value: id, label: displayMatchValue(c, 'Title', 'title') || id };
                    })}
                    onChange={v => setForm({ ...form, clientId: v })}
                    ariaLabel="Cliente"
                  />
                </div>
              ) : (
                <div className="negocio-modal-grid-auto">
                  <div className="form-group">
                    <label>Identificação Cliente</label>
                    <input type="text" value={form.clientTitle} onChange={e => setForm({ ...form, clientTitle: e.target.value })} />
                  </div>
                  <div className="form-group form-group-full">
                    <label>Tipo de cliente</label>
                    <ChoiceChipField
                      label="Tipo de cliente"
                      options={INV_TIPO_CLIENTE}
                      value={form.tipoCliente || ''}
                      onChange={next => setForm({ ...form, tipoCliente: next })}
                    />
                  </div>
                  <div className="form-group">
                    <label>Email</label>
                    <EmailInputField
                      label="Email"
                      errorId="wizard-email-error"
                      value={form.email}
                      onChange={next => setForm({ ...form, email: next })}
                    />
                  </div>
                  <div className="form-group">
                    <label>Contacto</label>
                    <ContactoInputField
                      label="Contacto"
                      value={form.contacto}
                      onChange={next => setForm({ ...form, contacto: next })}
                    />
                  </div>
                </div>
              )}
            </>
          )}
          {step === 2 && (
            <div className="negocio-modal-grid-auto">
              <div className="form-group">
                <label>Identificação Oportunidade</label>
                <input
                  type="text"
                  value={form.title}
                  placeholder={clientName ? `${clientName} · perfil` : 'Gerada automaticamente se vazio'}
                  onChange={e => setForm({ ...form, title: e.target.value })}
                />
              </div>
              <div className="form-group form-group-full">
                <label>Operação</label>
                <div className="segmented-control" role="radiogroup" aria-label="Operação">
                  {INV_OPERACAO.map(opt => (
                    <button
                      key={opt}
                      type="button"
                      className={`segmented-option${form.operacao === opt ? ' active' : ''}`}
                      aria-pressed={form.operacao === opt}
                      onClick={() => setForm({ ...form, operacao: opt, operacaoLabel: opt })}
                    >
                      {opt}
                    </button>
                  ))}
                </div>
              </div>
              <div className="form-group form-group-full">
                <RangeSliderField
                  label="Área (m²)"
                  min={INV_SLIDER_AREA.min}
                  max={INV_SLIDER_AREA.max}
                  step={INV_SLIDER_AREA.step}
                  minValue={form.areaMin}
                  maxValue={form.areaMax}
                  formatValue={n => `${formatNumber(n)} m²`}
                  onChange={(lo, hi) => setForm({ ...form, areaMin: lo, areaMax: hi })}
                />
              </div>
              <div className="form-group form-group-full">
                <RangeSliderField
                  label="Valor (€)"
                  min={INV_SLIDER_VALOR.min}
                  max={INV_SLIDER_VALOR.max}
                  step={INV_SLIDER_VALOR.step}
                  minValue={form.valorMin}
                  maxValue={form.valorMax}
                  formatValue={n => formatCurrency(n)}
                  onChange={(lo, hi) => setForm({ ...form, valorMin: lo, valorMax: hi })}
                />
              </div>
              <div className="form-group form-group-full">
                <RangeSliderField
                  label="Rendimento (%)"
                  min={INV_SLIDER_REND.min}
                  max={INV_SLIDER_REND.max}
                  step={INV_SLIDER_REND.step}
                  minValue={form.rendimentoMin}
                  maxValue={form.rendimentoMax}
                  formatValue={n => `${String(Math.round(n * 10) / 10).replace('.', ',')} %`}
                  onChange={(lo, hi) => setForm({ ...form, rendimentoMin: lo, rendimentoMax: hi })}
                />
              </div>
              <div className="form-group form-group-full">
                <label>Tipo de ativo</label>
                <InvMultiChoiceField fieldKey="TipoAtivoMatch" value={form.tipos} onChange={next => setForm({ ...form, tipos: next })} />
              </div>
              <div className="form-group form-group-full">
                <label>Localização</label>
                <InvMultiChoiceField
                  fieldKey="LocalizacaoMatch"
                  value={form.localizacoes}
                  extraChoices={locationChoices}
                  onChange={next => setForm({ ...form, localizacoes: next })}
                />
              </div>
              <div className="form-group form-group-full">
                <label>Potencial de oportunidade</label>
                <InvMultiChoiceField fieldKey="PotencialOportunidade" value={form.potencial} onChange={next => setForm({ ...form, potencial: next })} />
              </div>
            </div>
          )}
          {step === 3 && (
            <>
              {!duplicates.length && (
                <p className="form-hint">Nenhuma oportunidade com o mesmo perfil (operação + tipo + localização). Pode criar uma nova.</p>
              )}
              {duplicates.length > 0 && (
                <>
                  <p className="form-hint">Já existe {duplicates.length === 1 ? 'uma oportunidade' : `${duplicates.length} oportunidades`} com o mesmo perfil. Associe o cliente a uma existente para evitar dados duplicados.</p>
                  {duplicates.map((opp) => {
                    const p = extractInvProfile(opp, 'opp');
                    return (
                      <article key={opp.id || opp.Id} className="matching-card">
                        <div className="matching-card-top">
                          <strong className="matching-card-title">{p.title}</strong>
                          <button type="button" className="btn btn-primary btn-sm" disabled={saving} onClick={() => signExisting(opp)}>
                            Associar cliente
                          </button>
                        </div>
                        <div className="matching-card-meta">
                          {p.clientes && <span>{p.clientes}</span>}
                          {p.operacao && <span>{p.operacao}</span>}
                          {p.tipos.length > 0 && <span>{p.tipos.join(', ')}</span>}
                          {p.localizacoes.length > 0 && <span>{p.localizacoes.join(', ')}</span>}
                        </div>
                      </article>
                    );
                  })}
                </>
              )}
            </>
          )}
        </div>
        <div className="negocio-modal-footer">
          <button type="button" className="btn btn-secondary" onClick={step === 1 ? onClose : () => setStep(s => s - 1)} disabled={saving}>
            {step === 1 ? 'Cancelar' : 'Voltar'}
          </button>
          {step === 1 && (
            <button
              type="button"
              className="btn btn-primary"
              disabled={!canStep1}
              onClick={() => {
                if (clientMode === 'new' && emailFieldError(form.email)) {
                  toast.warning(emailFieldError(form.email));
                  return;
                }
                setStep(2);
              }}
            >
              Continuar
            </button>
          )}
          {step === 2 && (
            <button type="button" className="btn btn-primary" disabled={!canStep2} onClick={runDuplicateCheck}>Verificar perfil</button>
          )}
          {step === 3 && (
            <button type="button" className="btn btn-primary" disabled={saving || !checked} onClick={createNew}>
              {saving ? 'A guardar…' : 'Criar mesmo assim'}
            </button>
          )}
        </div>
      </div>
    </NegocioModalOverlay>
  );
}

function IntermediacaoPage({ user, onConfigureListColumns }) {
  return (
    <GenericListPage
      user={user}
      listKey="intermediacao"
      title="Intermediação"
      icon="bank"
      columns={INTERMEDIACAO_COLUMNS}
      onConfigureListColumns={onConfigureListColumns}
      hideTitleField
      enableEstadoFilter
      enablePeriodFilter
      showMoneyTotals
    />
  );
}

function AngariacaoPage({ user, onConfigureListColumns }) {
  return (
    <GenericListPage
      user={user}
      listKey="angariacao"
      title="Angariação"
      icon="angariacao"
      columns={ANGARIACAO_COLUMNS}
      onConfigureListColumns={onConfigureListColumns}
      hideTitleField
      enableEstadoFilter
      enablePeriodFilter
      showMoneyTotals
    />
  );
}

function SoiQuarterCell({ quarter, canEdit, canCreate, onEdit, onCreate }) {
  const indicators = quarter?.indicators || quarter?.Indicators || [];
  const score = quarter?.score ?? quarter?.Score;
  const tone = soiTone(score);
  const itemId = quarter?.itemId || quarter?.ItemId;
  return (
    <div className={`soi-quarter tone-${tone}`}>
      <div className="soi-quarter-head">
        <span className={`soi-gro soi-gro-${tone}`}>{formatSoiGro(score)}</span>
        {itemId && canEdit && (
          <button type="button" className="btn btn-secondary btn-sm" onClick={onEdit}>Editar</button>
        )}
        {!itemId && canCreate && (
          <button type="button" className="btn btn-secondary btn-sm" onClick={onCreate}>Definir</button>
        )}
      </div>
      <ul className="soi-quarter-metrics">
        <li className="soi-quarter-metrics-head" aria-hidden="true">
          <span className="soi-metric-label" />
          <span className="soi-metric-pair">
            <span><strong>Efetivo</strong></span>
            <span className="soi-metric-slash">/</span>
            <span><strong>Objetivo</strong></span>
          </span>
          <em>GRO</em>
        </li>
        {SOI_OBJECTIVE_FIELDS.map(field => {
          const ind = indicators.find(i => (i.key || i.Key) === field.key);
          const objetivo = ind?.objetivo ?? ind?.Objetivo;
          const cumprimento = ind?.cumprimento ?? ind?.Cumprimento ?? 0;
          const gro = ind?.gro ?? ind?.Gro;
          return (
            <li key={field.key}>
              <span className="soi-metric-label">{field.label}</span>
              <AppTooltip label="Efetivo / Objetivo">
                <span className="soi-metric-pair">
                  <strong>{formatSoiAmount(cumprimento, field.money)}</strong>
                  <span className="soi-metric-slash" aria-hidden="true">/</span>
                  <strong>{formatSoiAmount(objetivo, field.money)}</strong>
                </span>
              </AppTooltip>
              <AppTooltip label="GRO = efetivo ÷ objetivo">
                <em className={`soi-gro-${soiTone(gro)}`}>{formatSoiGro(gro)}</em>
              </AppTooltip>
            </li>
          );
        })}
      </ul>
    </div>
  );
}

function SoiObjectiveModal({ editor, user, operacaoOptions, saving, onClose, onSave }) {
  const [form, setForm] = useState(editor);
  const canChangeConsultor = userCanChangeConsultor(user);
  const setValue = (key, value) => setForm(prev => ({
    ...prev,
    values: { ...prev.values, [key]: value },
  }));

  const handleSave = () => {
    if (form.mode === 'create') {
      if (!form.consultorLookupId) {
        toast.warning('Selecione o consultor.');
        return;
      }
      if (!form.operacaoLookupId) {
        toast.warning('Selecione a operação.');
        return;
      }
    }
    const payload = {};
    SOI_OBJECTIVE_FIELDS.forEach(field => {
      payload[field.key] = parseSoiObjective(form.values[field.key]);
    });
    onSave(form, payload);
  };

  const dismiss = useOverlayDismiss(() => { if (!saving) onClose(); });

  return ReactDOM.createPortal(
    <div className="negocio-modal-overlay" role="presentation" {...dismiss}>
      <div className="negocio-modal" role="dialog" aria-modal="true" aria-labelledby="soi-modal-title" onClick={e => e.stopPropagation()}>
        <div className="negocio-modal-panel">
          <div className="negocio-modal-header">
            <div>
              <h3 id="soi-modal-title">{form.mode === 'create' ? 'Novo objetivo' : 'Editar objetivos'}</h3>
              <p className="negocio-modal-subtitle">O cumprimento e o GRO são calculados. Aqui só se grava o objetivo.</p>
            </div>
            <button type="button" className="negocio-modal-close" onClick={onClose} aria-label="Fechar" disabled={saving}>
              <Icon name="close" size={18} />
            </button>
          </div>
          <div className="negocio-modal-body">
            {form.mode === 'edit' && (
              <p className="soi-edit-context">{form.consultorLabel} · {form.operacaoLabel} · T{form.trimestre} {form.ano}</p>
            )}
            {form.mode === 'create' && (
              <>
                <div className="form-group">
                  <label>Consultor</label>
                  <ConsultorSearchField
                    value={form.consultorLookupId}
                    labelText={form.consultorLabel}
                    disabled={!canChangeConsultor}
                    onChange={({ lookupId, label }) => setForm(prev => ({
                      ...prev,
                      consultorLookupId: lookupId,
                      consultorLabel: label,
                    }))}
                  />
                </div>
                <div className="form-group">
                  <label>Operação</label>
                  <AppSelect
                    value={form.operacaoLookupId}
                    emptyOption="Selecionar"
                    options={operacaoOptions.map(op => ({ value: op.id, label: op.label }))}
                    onChange={v => setForm(prev => ({ ...prev, operacaoLookupId: v }))}
                    ariaLabel="Operação"
                  />
                </div>
                <div className="form-group">
                  <label>Trimestre</label>
                  <AppSelect
                    value={form.trimestre}
                    options={[1, 2, 3, 4].map(q => ({ value: q, label: `T${q}` }))}
                    onChange={v => setForm(prev => ({ ...prev, trimestre: Number(v) }))}
                    ariaLabel="Trimestre"
                  />
                </div>
              </>
            )}
            {SOI_OBJECTIVE_FIELDS.map(field => (
              <div className="form-group" key={field.key}>
                <label>{field.label}</label>
                <input
                  inputMode="decimal"
                  value={form.values[field.key]}
                  onChange={e => setValue(field.key, e.target.value)}
                  placeholder={field.money ? '0,00' : '0'}
                />
              </div>
            ))}
          </div>
          <div className="negocio-modal-footer">
            <button type="button" className="btn btn-secondary" onClick={onClose} disabled={saving}>Cancelar</button>
            <button type="button" className="btn btn-primary" onClick={handleSave} disabled={saving}>
              {saving ? 'A guardar…' : 'Guardar objetivos'}
            </button>
          </div>
        </div>
      </div>
    </div>,
    document.body
  );
}

function SoiPage({ user, onConfigureListColumns }) {
  const yearNow = new Date().getFullYear();
  const [ano, setAno] = useState(yearNow);
  const [operacao, setOperacao] = useState('');
  const [search, setSearch] = useState('');
  const [card, setCard] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [editor, setEditor] = useState(null);
  const [saving, setSaving] = useState(false);
  const [operacaoOptions, setOperacaoOptions] = useState([]);

  useEffect(() => {
    let cancelled = false;
    loadOperacaoOptions(user).then(opts => { if (!cancelled) setOperacaoOptions(opts || []); }).catch(() => {});
    return () => { cancelled = true; };
  }, [user]);

  const load = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const params = new URLSearchParams({ ano: String(ano) });
      if (operacao) params.set('operacao', operacao);
      const res = await apiFetch(`/api/soi/scorecard?${params}`);
      setCard(res);
    } catch (err) {
      setError(err);
      setCard(null);
    } finally {
      setLoading(false);
    }
  }, [ano, operacao]);

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

  const groups = (card?.groups || card?.Groups || []).filter(g => {
    const name = String(g.consultor || g.Consultor || '').toLowerCase();
    const q = search.trim().toLowerCase();
    return !q || name.includes(q);
  });
  const canEdit = !!(card?.canEdit || card?.CanEdit);
  const canCreate = !!(card?.canCreate || card?.CanCreate);
  const years = [yearNow - 1, yearNow, yearNow + 1];

  const openCreate = async (trimestre = 1, operacaoLabel = '') => {
    const match = operacaoOptions.find(o => String(o.label).toLowerCase() === String(operacaoLabel || '').toLowerCase());
    let consultorLookupId = '';
    let consultorLabel = '';
    if (!userCanChangeConsultor(user)) {
      const self = await resolveSelfConsultorPrefill(user);
      if (self) {
        consultorLookupId = self.consultorLookupId;
        consultorLabel = self.consultorLabel;
      }
    }
    setEditor({
      mode: 'create',
      trimestre,
      ano,
      consultorLookupId,
      consultorLabel,
      operacaoLookupId: match?.id || '',
      values: blankSoiObjectives(),
    });
  };

  const openEdit = (group, quarter) => {
    setEditor({
      mode: 'edit',
      itemId: quarter.itemId || quarter.ItemId,
      trimestre: quarter.quarter || quarter.Quarter,
      ano,
      consultorLabel: group.consultor || group.Consultor,
      operacaoLabel: group.operacao || group.Operacao,
      values: soiObjectivesFromQuarter(quarter),
    });
  };

  const handleSave = async (form, payload) => {
    setSaving(true);
    try {
      if (form.mode === 'create') {
        await apiFetch('/api/soi/objectives', {
          method: 'POST',
          body: JSON.stringify({
            consultorLookupId: Number(form.consultorLookupId),
            operacaoLookupId: Number(form.operacaoLookupId),
            trimestre: Number(form.trimestre),
            ano: Number(form.ano),
            ...payload,
          }),
        });
        toast.success('Objetivo criado.');
      } else {
        await apiFetch(`/api/soi/objectives/${encodeURIComponent(form.itemId)}`, {
          method: 'PATCH',
          body: JSON.stringify(payload),
        });
        toast.success('Objetivos guardados.');
      }
      setEditor(null);
      await load();
    } catch (err) {
      toast.error(err.message || 'Erro ao guardar objetivos.');
    } finally {
      setSaving(false);
    }
  };

  return (
    <>
      <ModuleTopbar
        icon="soi"
        title="SOI"
        user={user}
        actions={canCreate ? (
          <button type="button" className="btn btn-primary" onClick={() => openCreate(Math.floor(new Date().getMonth() / 3) + 1, operacao)}>
            <Icon name="plus" size={16} color="#FFFFFF" />
            Novo objetivo
          </button>
        ) : null}
      />
      <div className="page-body">
        <div className="page-content module-layout">
          <ModulePageShell
            toolbar={(
              <div className="toolbar mediacao-toolbar">
                <div className="toolbar-filters">
                  {!userIsConsultor(user) && (
                    <input
                      placeholder="Pesquisar consultor"
                      aria-label="Pesquisar consultor"
                      value={search}
                      onChange={e => setSearch(e.target.value)}
                    />
                  )}
                  <AppSelect
                    variant="toolbar"
                    ariaLabel="Ano"
                    value={ano}
                    options={years.map(y => ({ value: y, label: String(y) }))}
                    onChange={v => setAno(Number(v))}
                  />
                  <AppSelect
                    variant="toolbar"
                    ariaLabel="Operação"
                    value={operacao}
                    emptyOption="Todas as operações"
                    options={operacaoOptions.map(op => ({ value: op.label, label: op.label }))}
                    onChange={setOperacao}
                  />
                </div>
                <div className="toolbar-actions">
                  <MapearColunasToolbarButton user={user} listKey="soi" onConfigureListColumns={onConfigureListColumns} />
                  <button type="button" className="btn btn-secondary toolbar-icon-btn" onClick={load} title="Atualizar">
                    <Icon name="refresh" size={16} />
                  </button>
                </div>
              </div>
            )}
          >
            {loading && <LoadingSpinner message="A carregar objetivos..." />}
            {!loading && error && <ErrorBanner error={error} onDismiss={() => setError(null)} />}
            {!loading && !error && groups.length === 0 && (
              <p className="empty-hint">Ainda não há objetivos para {ano}.</p>
            )}
            {!loading && !error && groups.length > 0 && (
              <div className="module-page-table soi-scorecard-wrap">
                <p className="soi-scorecard-legend" aria-label="Legenda SOI">
                  <span><strong>Efetivo</strong> valor realizado</span>
                  <span><strong>Objetivo</strong> meta do trimestre</span>
                  <span><strong>GRO</strong> efetivo ÷ objetivo</span>
                </p>
                <table className="data-table soi-scorecard">
                  <thead>
                    <tr>
                      <th>Consultor</th>
                      <th>Operação</th>
                      {[1, 2, 3, 4].map(q => <th key={q}>T{q}</th>)}
                    </tr>
                  </thead>
                  <tbody>
                    {groups.map(group => {
                      const quarters = group.quarters || group.Quarters || [];
                      const name = group.consultor || group.Consultor || '—';
                      const op = group.operacao || group.Operacao || '—';
                      return (
                        <tr key={`${group.email || group.Email || name}|${op}`}>
                          <td className="soi-scorecard-person">{name}</td>
                          <td>{op}</td>
                          {[1, 2, 3, 4].map(q => {
                            const quarter = quarters.find(item => (item.quarter || item.Quarter) === q) || { quarter: q };
                            return (
                              <td key={q}>
                                <SoiQuarterCell
                                  quarter={quarter}
                                  canEdit={canEdit}
                                  canCreate={canCreate}
                                  onEdit={() => openEdit(group, quarter)}
                                  onCreate={() => openCreate(q, op === '—' ? '' : op)}
                                />
                              </td>
                            );
                          })}
                        </tr>
                      );
                    })}
                  </tbody>
                </table>
              </div>
            )}
          </ModulePageShell>
        </div>
      </div>
      {editor && (
        <SoiObjectiveModal
          editor={editor}
          user={user}
          operacaoOptions={operacaoOptions}
          saving={saving}
          onClose={() => { if (!saving) setEditor(null); }}
          onSave={handleSave}
        />
      )}
    </>
  );
}

function CarteiraPage({ user, activeTab, onTabChange, onConfigureListColumns }) {
  const tabId = carteiraTabFromRoute(activeTab);
  const tab = CARTEIRA_TABS.find(t => t.id === tabId) || CARTEIRA_TABS.find(t => t.id === 'ativos') || CARTEIRA_TABS[0];
  const [wizardOpen, setWizardOpen] = useState(false);
  const [listEpoch, setListEpoch] = useState(0);

  const tabsSlot = (
    <div className="tabs tabs-segmented module-tabs" role="tablist" aria-label="Secções Investimentos">
      {CARTEIRA_TABS.map(t => (
        <button
          key={t.id}
          type="button"
          className={`tab ${tab.id === t.id ? 'active' : ''}`}
          role="tab"
          aria-selected={tab.id === t.id}
          onClick={() => onTabChange(t.slug)}
        >
          {t.label}
        </button>
      ))}
    </div>
  );

  const refreshLists = () => setListEpoch(k => k + 1);

  if (tab.id === 'matching') {
    return (
      <>
        <ModuleTopbar icon="investimentos" title="Investimentos" user={user} />
        <div className="page-body">
          <div className="page-content module-layout">
            <ModulePageShell tabs={tabsSlot}>
              <CarteiraMatchingPage
                key={`matching-${listEpoch}`}
                user={user}
                embedded
                onConfigureListColumns={onConfigureListColumns}
              />
            </ModulePageShell>
          </div>
        </div>
      </>
    );
  }

  if (tab.id === 'clientes') {
    return (
      <>
        <ModuleTopbar icon="investimentos" title="Investimentos" user={user} />
        <div className="page-body">
          <div className="page-content module-layout">
            <ModulePageShell tabs={tabsSlot}>
              <InvestimentosClientesPage
                key={`clientes-${listEpoch}`}
                user={user}
                onConfigureListColumns={onConfigureListColumns}
              />
            </ModulePageShell>
          </div>
        </div>
      </>
    );
  }

  return (
    <>
      <ModuleTopbar icon="investimentos" title="Investimentos" user={user} />
      <GenericListPage
        key={`${tab.id}-${listEpoch}`}
        user={user}
        listKey={tab.id}
        columns={tab.columns}
        hideHeader
        headerSlot={tabsSlot}
        onConfigureListColumns={onConfigureListColumns}
        onCreateClick={tab.id === 'oportunidades' ? () => setWizardOpen(true) : null}
        defaultListView="cards"
      />
      <NovaOportunidadeWizard
        user={user}
        open={wizardOpen}
        onClose={() => setWizardOpen(false)}
        onCreated={refreshLists}
      />
    </>
  );
}

function ModuleDisabledMessage({ moduleName }) {
  return <div className="module-disabled">Módulo {moduleName} desativado nas definições.</div>;
}

function formatKpiValue(kpi) {
  if (!kpi) return '—';
  const val = kpi.value ?? kpi.Value;
  if (val == null) return '—';
  const fmt = kpi.format ?? kpi.Format ?? 'number';
  if (fmt === 'currency') return formatCurrency(val);
  return String(Math.round(val));
}

function formatTrend(changePercent, filterHint = '', previousValue = null) {
  const hint = String(filterHint || '').toLowerCase();
  const prev = previousValue != null ? Number(previousValue) : null;
  const change = changePercent != null ? Number(changePercent) : null;

  // Guard stale API responses that faked +100% when previous month was zero.
  if (change === 100 && (prev === 0 || prev == null || Number.isNaN(prev))) {
    if (hint.includes('em curso') || hint.includes('estado aberto')) {
      return { text: 'Sem comparação mensal', positive: null };
    }
    return { text: '—', positive: null };
  }

  // 0 this month vs a real previous month is "ainda sem dados", not -100%.
  if (change === -100) {
    return { text: 'Sem dados este mês', positive: null };
  }

  if (changePercent == null || changePercent === undefined || Number.isNaN(change)) {
    if (prev > 0) {
      return { text: 'Sem dados este mês', positive: null };
    }
    if (hint.includes('em curso') || hint.includes('estado aberto')) {
      return { text: 'Sem comparação mensal', positive: null };
    }
    return { text: '—', positive: null };
  }

  const sign = change > 0 ? '+' : '';
  return {
    text: `${sign}${change}% vs mês anterior`,
    positive: change > 0 ? true : change < 0 ? false : null,
  };
}

function CardLoading({ message = 'A carregar...' }) {
  return (
    <div className="card-loading" role="status" aria-live="polite">
      <LoadingSpinner message={message} />
    </div>
  );
}

function DashboardFilterStar({ hint }) {
  const text = (hint || '').trim();
  if (!text) return null;
  return (
    <abbr
      className="dashboard-filter-star"
      title={text}
      aria-label={`Filtro: ${text}`}
    >
      *
    </abbr>
  );
}

function DashboardKpiCard({ kpi, icon, loading, onExplore = null, onInspect = null, period = '' }) {
  const filterHint = kpi?.filterHint ?? kpi?.FilterHint ?? '';
  const trend = formatTrend(
    kpi?.changePercent ?? kpi?.ChangePercent,
    filterHint,
    kpi?.previousValue ?? kpi?.PreviousValue,
  );
  const clickable = typeof onExplore === 'function' && !loading;
  // Keep the admin eye visible as soon as inspect is allowed — do not wait for KPI load.
  const canInspect = typeof onInspect === 'function';
  // Estado only — month/year lives in the card footer.
  const filterSummary = (() => {
    const text = String(filterHint || '').trim();
    if (!text) return '';
    const parts = text.split('·').map(s => s.trim()).filter(Boolean)
      .filter(s => !/^Mês\s+/i.test(s) && !/^\d{4}$/.test(s));
    const estadoPart = parts.find(s =>
      /^(Estados?:|Estado\b)/i.test(s) ||
      /em curso|conclu|perdido|aberto|ganho/i.test(s)
    );
    return estadoPart || '';
  })();
  return (
    <DashCard
      className={`kpi-card-v2${clickable ? ' clickable' : ''}`}
      icon={icon}
      titleTip={kpi?.label ?? kpi?.Label ?? ''}
      title={(
        <>
          {kpi?.label ?? kpi?.Label ?? '—'}
          <DashboardFilterStar hint={filterHint} />
        </>
      )}
      period={period}
      onExplore={clickable ? onExplore : undefined}
      onClick={clickable ? onExplore : undefined}
      role={clickable ? 'button' : undefined}
      tabIndex={clickable ? 0 : undefined}
      onKeyDown={clickable ? (e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          onExplore();
        }
      } : undefined}
    >
      {canInspect && (
        <button
          type="button"
          className="dashboard-inspect-btn"
          title="Ver filtros e dados (Admin)"
          aria-label="Ver filtros e dados"
          onClick={(e) => { e.stopPropagation(); onInspect(); }}
        >
          <Icon name="eye" size={14} />
        </button>
      )}
      {loading ? (
        <CardLoading message="" />
      ) : (
        <>
          <div className="kpi-card-v2-body">
            {filterSummary ? (
              <AppTooltip label={filterHint || filterSummary}>
                <span className="kpi-card-v2-filter-summary">{filterSummary}</span>
              </AppTooltip>
            ) : null}
            <div className="kpi-card-v2-value">{formatKpiValue(kpi)}</div>
          </div>
          <div className="kpi-card-v2-footer">
            <div className={`kpi-trend ${trend.positive === true ? 'positive' : trend.positive === false ? 'negative' : 'neutral'}`}>
              {trend.text}
            </div>
          </div>
        </>
      )}
    </DashCard>
  );
}

function activityBadgeClass(type) {
  const tone = valueTone(type);
  return tone ? `activity-badge activity-badge--${tone}` : 'activity-badge';
}

function pipelineBadgeClass(natureza) {
  const tone = valueTone(natureza);
  return tone === 'venda' || tone === 'arrendamento'
    ? `pipeline-badge pipeline-badge--${tone}`
    : 'pipeline-badge';
}

const PIPELINE_COMPACT_WIDTH = 600;

function pipelineColumnEstado(col) {
  return col?.estado ?? col?.Estado ?? '—';
}

function PipelineKanban({ columns, loading, visibleEstados = null }) {
  const rootRef = useRef(null);
  const [compact, setCompact] = useState(
    () => typeof window !== 'undefined' && window.innerWidth <= BP_PHONE_MAX
  );
  const [activeEstado, setActiveEstado] = useState('Em Curso');

  useEffect(() => {
    const el = rootRef.current;
    if (!el || typeof ResizeObserver === 'undefined') return undefined;
    const apply = (width) => setCompact(width > 0 && width < PIPELINE_COMPACT_WIDTH);
    apply(el.getBoundingClientRect().width);
    const ro = new ResizeObserver((entries) => {
      apply(entries[0]?.contentRect?.width ?? 0);
    });
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  let cols = columns || [];
  if (Array.isArray(visibleEstados) && visibleEstados.length > 0) {
    const allow = new Set(visibleEstados.map(s => String(s).trim().toLowerCase()));
    cols = cols.filter(col => allow.has(String(pipelineColumnEstado(col)).trim().toLowerCase()));
  }

  const tabEstados = cols.map(pipelineColumnEstado);
  const selected = tabEstados.includes(activeEstado)
    ? activeEstado
    : (tabEstados.includes('Em Curso') ? 'Em Curso' : tabEstados[0]);
  const displayCols = !loading && compact && cols.length > 1
    ? cols.filter(col => pipelineColumnEstado(col) === selected)
    : cols;

  const renderColumn = (col) => {
    const estado = pipelineColumnEstado(col);
    const total = col.total ?? col.Total ?? 0;
    const count = col.count ?? col.Count ?? 0;
    const items = col.items ?? col.Items ?? [];
    return (
      <div key={estado} className="pipeline-column">
        <div className="pipeline-column-head">
          <span className="pipeline-column-title">{estado}</span>
          <span className="pipeline-column-meta">{count}</span>
        </div>
        <div className="pipeline-column-meta">{formatCurrency(total)}</div>
        {items.length === 0 ? (
          <p className="empty-hint">Sem negócios neste estado.</p>
        ) : items.map(item => {
          const natureza = item.naturezaNegocio ?? item.NaturezaNegocio;
          return (
            <div key={item.id ?? item.Id} className="pipeline-card">
              <div className="pipeline-card-title">{item.referencia ?? item.Referencia ?? item.consultor ?? item.Consultor ?? 'Negócio'}</div>
              <div className="pipeline-card-sub">{item.consultor ?? item.Consultor ?? '—'}</div>
              {item.montante != null && <div className="pipeline-card-value">{formatCurrency(item.montante ?? item.Montante)}</div>}
              {natureza && (
                <span className={pipelineBadgeClass(natureza)}>{natureza}</span>
              )}
            </div>
          );
        })}
      </div>
    );
  };

  return (
    <div ref={rootRef} className={`pipeline-board${compact ? ' is-compact' : ''}`}>
      {compact && !loading && cols.length > 1 && (
        <div className="pipeline-estado-pills" role="tablist" aria-label="Estado do pipeline">
          {cols.map(col => {
            const estado = pipelineColumnEstado(col);
            const count = col.count ?? col.Count ?? 0;
            const isActive = estado === selected;
            return (
              <button
                key={estado}
                type="button"
                role="tab"
                className={isActive ? 'active' : ''}
                aria-selected={isActive}
                onClick={() => setActiveEstado(estado)}
              >
                <span className="pipeline-pill-label">{estado}</span>
                <span className="pipeline-pill-count">{count}</span>
              </button>
            );
          })}
        </div>
      )}
      {loading ? (
        <CardLoading message="A carregar pipeline..." />
      ) : cols.length === 0 ? (
        <p className="empty-hint" style={{ padding: '1rem' }}>Sem negócios no pipeline.</p>
      ) : (
        <div className="pipeline-kanban">
          {displayCols.map(renderColumn)}
        </div>
      )}
    </div>
  );
}

function ComissaoProgressCard({ user, ano, onExplore = null }) {
  const email = user?.email || user?.Email;
  const year = Number(ano) || new Date().getFullYear();
  const [yearCommission, setYearCommission] = useState(null);
  const [loading, setLoading] = useState(true);
  // Own-dashboard admin preview: always Demo so layout placement matches Development.
  // "Entrar como" uses the consultor's user object, so they still see live commission.
  const isAdminPreview = userIsAdmin(user);

  useEffect(() => {
    let cancelled = false;
    // Admins without commission context use demo values (layout placement / preview).
    if (!email || isAdminPreview) {
      setYearCommission(null);
      setLoading(false);
      return undefined;
    }
    setLoading(true);
    (async () => {
      try {
        const data = await apiFetch(
          `/api/equipa/membro?email=${encodeURIComponent(email)}&ano=${encodeURIComponent(year)}`
        );
        if (!cancelled) setYearCommission(data.commission || data.Commission || null);
      } catch {
        if (!cancelled) setYearCommission(null);
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, [email, year, isAdminPreview]);

  const demoCommission = isAdminPreview
    ? {
        escalao: 1,
        sumComissao: 25770.25,
        limiteMin: 0,
        limiteMax: 49999,
        progressPct: 51.54,
        existeComissao: true,
        planoEscalao: 2,
        faixaCount: 4,
        faixaIndex: 1,
        ano: year,
        anoComissao: year,
      }
    : null;

  const source = yearCommission || demoCommission || {};
  const merged = {
    ...user,
    roles: [],
    escalao: source.escalao ?? source.Escalao ?? user?.escalao,
    objectivo: source.objectivo ?? source.Objectivo ?? user?.objectivo,
    sumComissao: source.sumComissao ?? source.SumComissao ?? user?.sumComissao,
    limiteMin: source.limiteMin ?? source.LimiteMin ?? user?.limiteMin,
    limiteMax: source.limiteMax ?? source.LimiteMax ?? user?.limiteMax,
    progressPct: source.progressPct ?? source.ProgressPct ?? user?.progressPct,
    existeComissao: source.existeComissao ?? source.ExisteComissao ?? user?.existeComissao,
    anoComissao: source.ano ?? source.anoComissao ?? source.Ano ?? year,
    planoEscalao: source.planoEscalao ?? source.PlanoEscalao ?? user?.planoEscalao,
    faixaCount: source.faixaCount ?? source.FaixaCount ?? user?.faixaCount,
    faixaIndex: source.faixaIndex ?? source.FaixaIndex ?? user?.faixaIndex,
  };

  const metrics = commissionMetrics(merged);
  const hasCommission = !!(merged.existeComissao ?? merged.ExisteComissao);
  const escalao = merged.escalao ?? merged.Escalao ?? null;
  const pct = Math.round(Math.min(100, Math.max(0, metrics.segmentPct)));
  const yearLabel = merged.anoComissao || year;

  const handleExplore = () => {
    if (typeof onExplore === 'function') {
      onExplore();
      return;
    }
    window.history.pushState(null, '', buildPageUrl('mediacao', 'comissao', { ano: year }));
    window.dispatchEvent(new PopStateEvent('popstate'));
  };

  return (
    <DashCard
      className={`comissao-dash-card${isAdminPreview ? ' is-demo' : ''}`}
      icon="chartProgress"
      title={(
        <>
          {userIsDiretorGeral(user) ? CANAL_HABITA_LABEL : 'Comissão'}
          {isAdminPreview && <span className="comissao-dash-demo-pill">Demo</span>}
        </>
      )}
      period={dashboardPeriodLabel(yearLabel, '')}
      onExplore={handleExplore}
    >
      <div className="comissao-dash-body">
        {loading ? (
          <CardLoading />
        ) : !hasCommission ? (
          <p className="empty-hint">Sem plano comissional para {yearLabel}.</p>
        ) : (
          <>
            <div className="comissao-dash-metrics">
              <div>
                <span className="perfil-metric-label">Escalão</span>
                <strong>{escalao != null ? escalao : '—'}</strong>
              </div>
              <div>
                <span className="perfil-metric-label">Comissão aprovada</span>
                <strong>{formatCurrency(metrics.sum)}</strong>
              </div>
              <div>
                <span className="perfil-metric-label">Progresso</span>
                <strong>{pct}%</strong>
              </div>
            </div>
            <div className="perfil-progress-block">
              <div className="perfil-progress-track" aria-hidden="true">
                <div className="perfil-progress-fill" style={{ width: `${pct}%` }} />
              </div>
              <div className="perfil-progress-ends">
                <span>{formatCurrency(metrics.min)}</span>
                <span>{metrics.max > metrics.min ? formatCurrency(metrics.max) : '—'}</span>
              </div>
            </div>
            <p className="perfil-hint">
              {isAdminPreview ? 'Pré-visualização admin · ' : ''}
              {escalao != null && metrics.faixaCount > 1
                ? `Escalão ${escalao}/${metrics.faixaCount}`
                : escalao != null
                  ? `Escalão ${escalao}`
                  : `Faixa ${yearLabel}`}
              {metrics.planoEscalao != null && metrics.planoEscalao !== escalao
                ? ` · plano ${metrics.planoEscalao}`
                : ''}
              {metrics.max > metrics.min
                ? `: ${formatCurrency(metrics.min)} – ${formatCurrency(metrics.max)}`
                : metrics.min > 0
                  ? `: ≥ ${formatCurrency(metrics.min)}`
                  : ''}
            </p>
          </>
        )}
      </div>
    </DashCard>
  );
}

/** @deprecated alias — dashboard still references ProgressCard id */
function ProgressCard(props) {
  return <ComissaoProgressCard {...props} />;
}

function ChallengeList({ pipeline, loading }) {
  if (loading) {
    return (
      <div className="ds-card">
        <div className="ds-card-head"><h3>Desafios do Mês</h3></div>
        <CardLoading />
      </div>
    );
  }
  const won = (pipeline || []).reduce((sum, col) => {
    const e = (col.estado ?? col.Estado ?? '').toLowerCase();
    if (e.includes('conclu') || e.includes('ganho')) return sum + (col.count ?? col.Count ?? 0);
    return sum;
  }, 0);
  const total = (pipeline || []).reduce((sum, col) => sum + (col.count ?? col.Count ?? 0), 0);
  const challenges = [
    { title: 'Fechar 5 negócios', current: Math.min(won, 5), target: 5, xp: 300 },
    { title: 'Pipeline ativo', current: Math.min(total, 10), target: 10, xp: 200 },
    { title: 'Comissão em curso', current: Math.min(won + 2, 20), target: 20, xp: 200 },
  ];
  return (
    <div className="ds-card">
      <div className="ds-card-head"><h3>Desafios do Mês</h3></div>
      <div className="challenge-list">
        {challenges.map(c => (
          <div key={c.title}>
            <div className="challenge-item-title">{c.title}</div>
            <div className="challenge-bar-track">
              <div className="challenge-bar-fill" style={{ width: `${Math.min(100, (c.current / c.target) * 100)}%` }} />
            </div>
            <div className="challenge-meta">
              <span>{c.current}/{c.target}</span>
              <span>{c.xp} XP</span>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

function LeaderboardRankMark({ rank, kind }) {
  const n = Number(rank) || 0;
  const isTop = n >= 1 && n <= 3;
  const isAntiguidade = kind === 'antiguidade';
  if (!isTop) {
    return <span className="leaderboard-rank">{n || '—'}</span>;
  }
  const starCount = isAntiguidade ? 1 : (4 - n);
  const label = isAntiguidade ? `${n}º em antiguidade` : `${n}º em comissão`;
  return (
    <AppTooltip label={label} className="leaderboard-rank-tip">
      <span
        className={`leaderboard-rank is-top rank-${n}${isAntiguidade ? ' is-calendar' : ' is-stars'}`}
        aria-label={label}
      >
        {isAntiguidade ? (
          <Icon name="calendar" size={10} color="#FFFFFF" />
        ) : (
          Array.from({ length: starCount }, (_, i) => (
            <Icon key={i} name="star" size={starCount > 1 ? 8 : 10} color="#FFFFFF" />
          ))
        )}
      </span>
    </AppTooltip>
  );
}

function Leaderboard({ ano = '', mes = '', user = null }) {
  const now = new Date();
  const hasDashMonth = mes !== '' && mes != null;
  const [kind, setKind] = useState('comissao');
  const [periodo, setPeriodo] = useState(hasDashMonth ? 'mes' : 'ano');
  const [mesLocal, setMes] = useState(Number(mes) || now.getMonth() + 1);
  const [rows, setRows] = useState([]);
  const [me, setMe] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const year = Number(ano) || now.getFullYear();
  const myEmail = String(user?.email || user?.Email || '').trim().toLowerCase();
  const isAntiguidade = kind === 'antiguidade';

  useEffect(() => {
    if (hasDashMonth) {
      setPeriodo('mes');
      setMes(Number(mes));
    }
  }, [mes, hasDashMonth]);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    setError(null);
    (async () => {
      try {
        const url = isAntiguidade
          ? '/api/dashboard/ranking-antiguidade?take=10'
          : (() => {
            const params = new URLSearchParams({ periodo, take: '10' });
            if (periodo !== 'sempre') params.set('ano', String(year));
            if (periodo === 'mes') params.set('mes', String(mesLocal));
            return `/api/dashboard/ranking?${params}`;
          })();
        const res = await apiFetch(url);
        if (cancelled) return;
        const items = (res.items || res.Items || [])
          .map((r, i) => ({
            rank: Number(r.rank ?? r.Rank ?? i + 1),
            email: (r.email || r.Email || '') ? String(r.email || r.Email).trim().toLowerCase() : '',
            name: String(r.name || r.Name || 'Consultor').split('|')[0].trim(),
            sum: Number(r.sum ?? r.Sum ?? 0) || 0,
            antiguidade: String(r.antiguidade || r.Antiguidade || '').trim(),
            count: Number(r.count ?? r.Count ?? 0) || 0,
            isMe: Boolean(r.isMe ?? r.IsMe),
          }))
          .filter(r => !/canal\s*habita/i.test(r.name) && !/canal\s*habita/i.test(r.email))
          .map((r, i) => ({ ...r, rank: i + 1 }));
        const mine = res.me || res.Me || null;
        setRows(items);
        setMe(mine ? {
          rank: Number(mine.rank ?? mine.Rank ?? 0),
          email: String(mine.email || mine.Email || '').trim().toLowerCase(),
          name: String(mine.name || mine.Name || '').split('|')[0].trim(),
          sum: Number(mine.sum ?? mine.Sum ?? 0) || 0,
          antiguidade: String(mine.antiguidade || mine.Antiguidade || '').trim(),
          count: Number(mine.count ?? mine.Count ?? 0) || 0,
          isMe: true,
        } : null);
      } catch (err) {
        if (!cancelled) {
          setRows([]);
          setMe(null);
          setError(err.message || 'Não foi possível carregar o ranking.');
        }
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, [kind, isAntiguidade, periodo, mesLocal, year]);

  const meInList = me && rows.some(r => r.email && me.email && r.email === me.email);
  const valueOf = (row) => (isAntiguidade ? (row.antiguidade || '—') : formatCurrency(row.sum));

  return (
    <DashCard
      className="leaderboard-card"
      icon={isAntiguidade ? 'calendar' : 'trophy'}
      title="Ranking"
      period={isAntiguidade ? '' : dashboardPeriodLabel(ano, mes)}
    >
      <div className="leaderboard-filters" onClick={e => e.stopPropagation()}>
        <div className="leaderboard-period leaderboard-kind" role="tablist" aria-label="Tipo de ranking">
          {[
            { id: 'comissao', label: 'Comissão' },
            { id: 'antiguidade', label: 'Antiguidade' },
          ].map(opt => (
            <button
              key={opt.id}
              type="button"
              role="tab"
              className={kind === opt.id ? 'active' : ''}
              aria-selected={kind === opt.id}
              onClick={() => setKind(opt.id)}
            >
              {opt.label}
            </button>
          ))}
        </div>
        {!isAntiguidade ? (
          <>
            <div className="leaderboard-period" role="tablist" aria-label="Período do ranking">
              {[
                { id: 'mes', label: 'Mês' },
                { id: 'ano', label: 'Ano' },
                { id: 'sempre', label: 'Sempre' },
              ].map(opt => (
                <button
                  key={opt.id}
                  type="button"
                  role="tab"
                  className={periodo === opt.id ? 'active' : ''}
                  aria-selected={periodo === opt.id}
                  aria-label={opt.id === 'sempre' ? 'Sempre — desde que está connosco' : opt.label}
                  onClick={() => setPeriodo(opt.id)}
                >
                  {opt.label}
                </button>
              ))}
            </div>
            {periodo === 'mes' ? (
              <DashboardPeriodSelect
                className="leaderboard-month"
                value={String(mesLocal)}
                options={DASH_MONTH_OPTIONS}
                onChange={v => setMes(Number(v) || now.getMonth() + 1)}
                ariaLabel="Filtrar ranking por mês"
              />
            ) : null}
          </>
        ) : null}
      </div>
      {loading ? (
        <CardLoading message="A carregar ranking..." />
      ) : error ? (
        <p className="empty-hint">{error}</p>
      ) : (
        <div className="leaderboard-list">
          <p className="leaderboard-period-hint">
            {isAntiguidade ? 'Colaboradores · anos de casa' : 'Consultores · comissão aprovada'}
          </p>
          {rows.length === 0 ? (
            <p className="empty-hint">
              {isAntiguidade ? 'Sem dados de antiguidade.' : 'Sem comissões aprovadas neste período.'}
            </p>
          ) : rows.map(row => {
            const isMe = row.isMe || (myEmail && row.email === myEmail);
            return (
              <div key={row.email || row.name} className={`leaderboard-row${isMe ? ' is-me' : ''}`}>
                <LeaderboardRankMark rank={row.rank} kind={kind} />
                <span className="leaderboard-name">{row.name}{isMe ? ' · eu' : ''}</span>
                <span className="leaderboard-sum">{valueOf(row)}</span>
              </div>
            );
          })}
          {me && !meInList && me.rank > 0 ? (
            <div className="leaderboard-row is-me is-self-rank">
              <LeaderboardRankMark rank={me.rank} kind={kind} />
              <span className="leaderboard-name">{me.name || 'Eu'} · eu</span>
              <span className="leaderboard-sum">{valueOf(me)}</span>
            </div>
          ) : null}
        </div>
      )}
    </DashCard>
  );
}

function formatActivityDay(value) {
  if (!value) return '';
  const raw = String(value);
  if (/^\d{2}:\d{2}$/.test(raw)) return '';
  const d = new Date(raw);
  if (Number.isNaN(d.getTime())) return raw;
  return d.toLocaleDateString('pt-PT', { weekday: 'short', day: '2-digit', month: '2-digit' });
}

async function loadWeekActivities(ano, mes) {
  const now = new Date();
  const year = Number(ano) || now.getFullYear();
  const month = Number(mes);
  const hasMonth = month >= 1 && month <= 12;
  const params = new URLSearchParams({ pageSize: '100' });
  if (ano) params.set('ano', String(year));
  if (hasMonth) params.set('mes', String(month));
  const res = await apiFetch(`/api/mediacao/negocios?${params}`).catch(() => ({ items: [] }));
  const items = res.items || res.Items || [];
  return items
    .filter(n => {
      const raw = n.data || n.Data;
      if (!raw) return false;
      const d = new Date(raw);
      if (Number.isNaN(d.getTime())) return false;
      if (ano && d.getFullYear() !== year) return false;
      if (hasMonth && d.getMonth() + 1 !== month) return false;
      return true;
    })
    .sort((a, b) => new Date(b.data || b.Data) - new Date(a.data || a.Data))
    .slice(0, 10)
    .map(n => ({
      title: n.referencia || n.Referencia || n.consultor || n.Consultor || 'Negócio',
      type: n.naturezaNegocio || n.NaturezaNegocio || 'Negócio',
      time: formatActivityDay(n.data || n.Data),
      operacao: n.operacao || n.Operacao || '',
      probabilidade: n.probabilidade || n.Probabilidade || '',
      consultor: n.consultor || n.Consultor || '',
    }));
}

function ActivityList({ items, emptyLabel, loading }) {
  if (loading) return <CardLoading />;
  const list = items || [];
  if (list.length === 0) return <p className="empty-hint">{emptyLabel}</p>;
  return (
    <div className="activity-list">
      {list.map((item, i) => {
        const type = item.type ?? item.Type ?? '—';
        return (
          <div key={i} className="activity-row">
            <div className="activity-row-main">
              <span className="activity-time">{formatActivityDay(item.time ?? item.Time) || item.time || item.Time || '—'}</span>
              <span className={activityBadgeClass(type)}>{type}</span>
              <span className="activity-title">{item.title ?? item.Title ?? '—'}</span>
            </div>
            <div className="activity-meta">
              {(item.operacao ?? item.Operacao) ? (
                <span className="activity-badge activity-badge--operacao">{item.operacao ?? item.Operacao}</span>
              ) : null}
              {(item.probabilidade ?? item.Probabilidade) ? (
                <span className={activityBadgeClass(item.probabilidade ?? item.Probabilidade)}>
                  {item.probabilidade ?? item.Probabilidade}
                </span>
              ) : null}
              {(item.consultor ?? item.Consultor) ? (
                <span className="activity-badge activity-badge--consultor">{item.consultor ?? item.Consultor}</span>
              ) : null}
            </div>
          </div>
        );
      })}
    </div>
  );
}

function currentSoiQuarter() {
  return Math.floor(new Date().getMonth() / 3) + 1;
}

function SoiScoreCard({ ano = '', mes = '', onExplore = null }) {
  const year = Number(ano) || new Date().getFullYear();
  const quarterFromMonth = mes
    ? Math.floor((Number(mes) - 1) / 3) + 1
    : currentSoiQuarter();
  const [trimestre, setTrimestre] = useState(quarterFromMonth);
  const [operacaoFilter, setOperacaoFilter] = useState('');
  const [summary, setSummary] = useState(null);
  const [loading, setLoading] = useState(true);
  const [showFilters, setShowFilters] = useState(false);
  const [popoverPos, setPopoverPos] = useState(null);
  const filterBtnRef = useRef(null);
  const popoverRef = useRef(null);

  useEffect(() => {
    setTrimestre(quarterFromMonth);
  }, [quarterFromMonth]);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    const params = new URLSearchParams({ ano: String(year), trimestre: String(trimestre) });
    if (operacaoFilter) params.set('operacao', operacaoFilter);
    apiFetch(`/api/dashboard/soi-summary?${params}`)
      .then(res => { if (!cancelled) setSummary(res); })
      .catch(() => { if (!cancelled) setSummary(null); })
      .finally(() => { if (!cancelled) setLoading(false); });
    return () => { cancelled = true; };
  }, [year, trimestre, operacaoFilter]);

  const openPopover = useCallback(() => {
    const btn = filterBtnRef.current;
    if (!btn) return;
    const r = btn.getBoundingClientRect();
    setPopoverPos({
      left: Math.max(12, Math.min(r.left, window.innerWidth - 280)),
      top: Math.min(r.bottom + 8, window.innerHeight - 280),
    });
    setShowFilters(true);
  }, []);
  const closePopover = useCallback(() => setShowFilters(false), []);

  useEffect(() => {
    if (!showFilters) return undefined;
    const onDoc = (e) => {
      if (popoverRef.current?.contains(e.target) || filterBtnRef.current?.contains(e.target)) return;
      closePopover();
    };
    const onKey = (e) => { if (e.key === 'Escape') closePopover(); };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDoc);
      document.removeEventListener('keydown', onKey);
    };
  }, [showFilters, closePopover]);

  const indicators = summary?.indicators || summary?.Indicators || [];
  const score = summary?.score ?? summary?.Score ?? 0;
  const status = summary?.status || summary?.Status || 'Sem objetivos';
  const insight = summary?.insight || summary?.Insight || 'Ainda não há objetivos para este trimestre.';
  const operacaoLabel = summary?.operacao || summary?.Operacao || '';
  const available = summary?.availableQuarters || summary?.AvailableQuarters || [];
  const operacoes = summary?.operacoes || summary?.Operacoes || [];
  const sameSoiOp = (a, b) => String(a || '').trim().toLowerCase() === String(b || '').trim().toLowerCase();
  const pickOperacao = (value) => {
    setOperacaoFilter(value);
    closePopover();
  };
  const tone = status === 'Sem objetivos' ? 'idle' : soiTone(score);
  const ring = tone === 'idle' ? 0 : Math.min(100, Number(score) || 0);
  const ringStyle = {
    background: `conic-gradient(var(--soi-ring-color) ${ring}%, rgba(31, 44, 92, 0.12) 0)`,
  };

  return (
    <DashCard
      className={`soi-card tone-${tone}${showFilters ? ' filters-open' : ''}`}
      icon="soi"
      title="SOI — Objetivos"
      period={dashboardPeriodLabel(ano, mes)}
      onExplore={onExplore}
      actions={(
        <div className="soi-card-actions">
          <AppTooltip label="Filtrar operação">
            <button
              type="button"
              className={`btn-filter-toggle btn-filter-chart${showFilters || operacaoFilter ? ' active' : ''}`}
              aria-pressed={showFilters}
              aria-label="Filtro de operação"
              ref={filterBtnRef}
              onClick={() => (showFilters ? closePopover() : openPopover())}
            >
              <Icon name="filter" size={14} />
              {operacaoFilter ? <span className="operacao-chart-filter-badge">1</span> : null}
            </button>
          </AppTooltip>
        </div>
      )}
    >
      <div className="soi-card-body">
        <div className="soi-quarter-tabs leaderboard-period" role="tablist" aria-label="Trimestre SOI">
          {[1, 2, 3, 4].map(q => {
            const hasData = available.includes(q);
            return (
              <button
                key={q}
                type="button"
                role="tab"
                className={trimestre === q ? 'active' : ''}
                aria-selected={trimestre === q}
                onClick={() => setTrimestre(q)}
              >
                T{q}
                {hasData ? <i className="soi-quarter-dot" aria-hidden="true" /> : null}
              </button>
            );
          })}
        </div>
        {loading ? (
          <CardLoading message="A carregar SOI..." />
        ) : (
          <>
            <div className="soi-hero">
              <div className="soi-activation-ring" style={ringStyle} aria-label={`GRO ${score}`}>
                <div className="soi-activation-core">
                  <strong>{tone === 'idle' ? '—' : score}</strong>
                  <span>GRO</span>
                </div>
              </div>
              <div className="soi-hero-copy">
                <div className="soi-hero-metric">
                  <strong>{`T${trimestre}`}</strong>
                  <span>{operacaoLabel || `Trimestre ${trimestre}`}</span>
                </div>
              </div>
              <p className="soi-insight">{insight}</p>
            </div>
            <div className="soi-bars" role="list">
              {SOI_OBJECTIVE_FIELDS.map(field => {
                const ind = indicators.find(i => (i.key || i.Key) === field.key);
                const gro = ind?.gro ?? ind?.Gro;
                const objetivo = ind?.objetivo ?? ind?.Objetivo;
                const cumprimento = ind?.cumprimento ?? ind?.Cumprimento;
                const width = gro == null ? 0 : Math.min(100, Number(gro) || 0);
                const barTone = soiTone(gro);
                const tip = `${field.label} · Efetivo ${formatSoiAmount(cumprimento, field.money)} · Objetivo ${formatSoiAmount(objetivo, field.money)} · GRO ${formatSoiGro(gro)}`;
                return (
                  <AppTooltip key={field.key} label={tip}>
                    <div className="soi-bar-row" role="listitem">
                      <div className="soi-bar-label">
                        <span>{field.label}</span>
                        <strong>{gro == null ? formatSoiAmount(cumprimento, field.money) : formatSoiGro(gro)}</strong>
                      </div>
                      <div className="soi-bar-track" aria-hidden="true">
                        <div className={`soi-bar-fill is-${barTone}`} style={{ width: `${width}%` }} />
                      </div>
                    </div>
                  </AppTooltip>
                );
              })}
            </div>
          </>
        )}
      </div>
      {showFilters && popoverPos && ReactDOM.createPortal(
        <aside
          ref={popoverRef}
          className="chart-filter-popover"
          aria-label="Filtro de operação SOI"
          style={{ left: popoverPos.left, top: popoverPos.top }}
        >
          <div className="chart-filter-popover-header">
            <div className="chart-filter-popover-title">Operação</div>
          </div>
          <div className="chart-filter-popover-body">
            <div className="checkbox-group">
              <span className="checkbox-group-title">Local</span>
              <div className="checkbox-group-chips">
                <label className="avaliacao-check">
                  <input
                    type="radio"
                    name="soi-card-operacao"
                    checked={!operacaoFilter}
                    onChange={() => pickOperacao('')}
                  />
                  Todas as operações
                </label>
                {operacoes.map(op => (
                  <label key={op} className="avaliacao-check">
                    <input
                      type="radio"
                      name="soi-card-operacao"
                      checked={sameSoiOp(operacaoFilter, op)}
                      onChange={() => pickOperacao(op)}
                    />
                    {op}
                  </label>
                ))}
              </div>
            </div>
          </div>
          <div className="chart-filter-popover-actions">
            <button type="button" className="btn btn-secondary btn-sm" onClick={() => pickOperacao('')}>
              Limpar
            </button>
          </div>
        </aside>,
        document.body
      )}
    </DashCard>
  );
}

const OPERACAO_PIE_COLORS = [
  '#1F2C5C', '#2D9C48', '#53AF3B', '#3D6B8C', '#268A3E',
  '#152A4A', '#7CB342', '#0F2744', '#6B8F71', '#A8D08D',
];

function operacaoPieColor(label, idx = 0) {
  const key = String(label || '')
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .trim()
    .toLowerCase();
  if (key.includes('lisboa')) return '#1F2C5C';
  if (key.includes('porto')) return '#2D9C48';
  if (key.includes('sines') || key.includes('alentejo')) return '#53AF3B';
  const i = Number(idx) || 0;
  return OPERACAO_PIE_COLORS[((i % OPERACAO_PIE_COLORS.length) + OPERACAO_PIE_COLORS.length) % OPERACAO_PIE_COLORS.length];
}

const OPERACAO_CHART_ESTADOS = ['Em Curso', 'Concluído', 'Perdido'];
const OPERACAO_CHART_PROBABILIDADES = ['Alta', 'Média', 'Baixa'];

function ProbabilidadeMultiSelect({ value = [], onChange, options = OPERACAO_CHART_PROBABILIDADES, variant = 'compact' }) {
  const selected = Array.isArray(value) ? value : [];
  const toggle = (opt) => {
    onChange(selected.includes(opt) ? selected.filter(x => x !== opt) : [...selected, opt]);
  };

  if (variant === 'chips') {
    return (
      <div className="checkbox-group">
        <span className="checkbox-group-title">Probabilidade</span>
        <div className="checkbox-group-chips">
          {options.map(opt => (
            <label key={opt} className="avaliacao-check">
              <input type="checkbox" checked={selected.includes(opt)} onChange={() => toggle(opt)} />
              {opt}
            </label>
          ))}
        </div>
      </div>
    );
  }

  return (
    <AppSelect
      variant="compact"
      label="Prob."
      multiple
      value={selected}
      options={options.map(opt => ({ value: opt, label: opt }))}
      onChange={onChange}
      ariaLabel="Probabilidade"
      placeholder="Todas"
    />
  );
}
const OPERACAO_CHART_METRICS = [
  { value: 'comissao', label: 'Comissão' },
  { value: 'montante', label: 'Montante' },
  { value: 'count', label: 'N.º negócios' },
];
const OPERACAO_CHART_DEFAULT_FILTERS = {
  metric: 'comissao',
  mes: '',
  naturezas: ['Venda', 'Arrendamento'],
  estados: ['Em Curso'],
  probabilidades: ['Alta'],
};

function sameFilterList(a, b) {
  const left = [...(a || [])].map(String).sort();
  const right = [...(b || [])].map(String).sort();
  return left.length === right.length && left.every((v, i) => v === right[i]);
}

function countAppliedFilters(applied, defaults) {
  if (!applied || !defaults) return 0;
  return Object.keys(defaults).reduce((n, key) => {
    const expected = defaults[key];
    const value = applied[key];
    if (Array.isArray(expected)) {
      return n + (sameFilterList(value, expected) ? 0 : 1);
    }
    return n + (String(value ?? '') === String(expected ?? '') ? 0 : 1);
  }, 0);
}

function polarToCartesian(cx, cy, r, angleDeg) {
  const rad = ((angleDeg - 90) * Math.PI) / 180;
  return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };
}

function describePieSlice(cx, cy, r, startAngle, endAngle) {
  if (endAngle - startAngle >= 359.999) {
    const mid = startAngle + 180;
    const a = polarToCartesian(cx, cy, r, startAngle);
    const b = polarToCartesian(cx, cy, r, mid);
    const c = polarToCartesian(cx, cy, r, endAngle);
    return [
      `M ${cx} ${cy}`,
      `L ${a.x} ${a.y}`,
      `A ${r} ${r} 0 1 1 ${b.x} ${b.y}`,
      `A ${r} ${r} 0 1 1 ${c.x} ${c.y}`,
      'Z',
    ].join(' ');
  }
  const start = polarToCartesian(cx, cy, r, endAngle);
  const end = polarToCartesian(cx, cy, r, startAngle);
  const large = endAngle - startAngle > 180 ? 1 : 0;
  return [
    `M ${cx} ${cy}`,
    `L ${end.x} ${end.y}`,
    `A ${r} ${r} 0 ${large} 1 ${start.x} ${start.y}`,
    'Z',
  ].join(' ');
}

function slicesFromRows(rows, field, colorOf) {
  const items = (rows || [])
    .map(r => ({ key: r.label, label: r.label, total: Number(r[field]) || 0 }))
    .filter(i => i.total > 0);
  const sum = items.reduce((acc, i) => acc + i.total, 0);
  let angle = 0;
  return items.map((i, idx) => {
    const sweep = sum > 0 ? (i.total / sum) * 360 : 0;
    const start = angle;
    angle += sweep;
    return {
      ...i,
      start,
      end: angle,
      pct: sum > 0 ? (i.total / sum) * 100 : 0,
      color: colorOf(i.label, idx),
    };
  });
}

function DashMiniPie({ slices, formatValue = formatCurrency, emptyLabel = 'Sem dados' }) {
  const [hoverKey, setHoverKey] = useState(null);
  const hovered = slices.find(s => s.key === hoverKey) || null;
  const pct = (n) => (n < 1 ? n.toFixed(1) : Math.round(n));
  const tip = hovered
    ? `${hovered.label} ${formatValue(hovered.total)} · ${pct(hovered.pct)}%`
    : (slices.length
      ? slices.map(s => `${s.label} ${formatValue(s.total)}`).join(' · ')
      : emptyLabel);
  return (
    <AppTooltip label={tip} className="dash-mini-pie-tip">
      <svg viewBox="0 0 200 200" className="operacao-pie-svg dash-mini-pie-svg" role="img">
        {!slices.length ? (
          <circle cx="100" cy="100" r="86" fill="none" stroke="rgba(31, 44, 92, 0.12)" strokeWidth="28" />
        ) : slices.map(s => (
          <g
            key={s.key}
            className={`operacao-pie-slice${hoverKey === s.key ? ' is-hot' : ''}${hoverKey && hoverKey !== s.key ? ' is-dim' : ''}`}
            onMouseEnter={() => setHoverKey(s.key)}
            onMouseLeave={() => setHoverKey(null)}
          >
            <path d={describePieSlice(100, 100, 86, s.start, s.end)} fill={s.color} />
          </g>
        ))}
        <circle cx="100" cy="100" r="52" className="operacao-pie-hole" />
        {hovered ? (
          <text x="100" y="105" textAnchor="middle" className="dash-mini-pie-center">{compactEuro(hovered.total)}</text>
        ) : null}
      </svg>
    </AppTooltip>
  );
}

function OperacaoPieChartCard({ ano = '', periodMes = '', user = null, isViewingAs = false, onInspect = null, onExplore = null }) {
  const wantConsultorMode = userIsConsultor(user) || isViewingAs || userHasCommissionProgress(user);
  const [items, setItems] = useState([]);
  const [chartMeta, setChartMeta] = useState({
    mode: 'por-operacao',
    localLabel: null,
    mineTotal: null,
    localTotal: null,
    partilhaTotal: null,
    homeOperacoes: [],
  });
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [showFilters, setShowFilters] = useState(false);
  const [metric, setMetric] = useState(OPERACAO_CHART_DEFAULT_FILTERS.metric);
  const [mes, setMes] = useState(OPERACAO_CHART_DEFAULT_FILTERS.mes);
  const [naturezas, setNaturezas] = useState(() => [...OPERACAO_CHART_DEFAULT_FILTERS.naturezas]);
  const [estados, setEstados] = useState(() => [...OPERACAO_CHART_DEFAULT_FILTERS.estados]);
  const [probabilidades, setProbabilidades] = useState(() => [...OPERACAO_CHART_DEFAULT_FILTERS.probabilidades]);
  const [applied, setApplied] = useState(() => ({
    metric: OPERACAO_CHART_DEFAULT_FILTERS.metric,
    mes: OPERACAO_CHART_DEFAULT_FILTERS.mes,
    naturezas: [...OPERACAO_CHART_DEFAULT_FILTERS.naturezas],
    estados: [...OPERACAO_CHART_DEFAULT_FILTERS.estados],
    probabilidades: [...OPERACAO_CHART_DEFAULT_FILTERS.probabilidades],
  }));
  const [hoverKey, setHoverKey] = useState(null);
  const filterBtnRef = useRef(null);
  const popoverRef = useRef(null);
  const [popoverPos, setPopoverPos] = useState(null);

  const load = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const params = new URLSearchParams();
      if (ano) params.set('ano', String(ano));
      if (applied.mes || periodMes) params.set('mes', String(applied.mes || periodMes));
      if (applied.metric) params.set('metric', applied.metric);
      if (applied.naturezas.length) params.set('naturezas', applied.naturezas.join(','));
      if (applied.estados.length) params.set('estados', applied.estados.join(','));
      if (applied.probabilidades?.length) params.set('probabilidades', applied.probabilidades.join(','));
      if (wantConsultorMode) params.set('mode', 'consultor');
      const res = await apiFetch(`/api/dashboard/comissao-por-operacao?${params}`);
      setItems(res.items || res.Items || []);
      setChartMeta({
        mode: res.mode || res.Mode || 'por-operacao',
        localLabel: res.localLabel || res.LocalLabel || null,
        mineTotal: res.mineTotal ?? res.MineTotal ?? null,
        localTotal: res.localTotal ?? res.LocalTotal ?? null,
        partilhaTotal: res.partilhaTotal ?? res.PartilhaTotal ?? null,
        homeOperacoes: res.homeOperacoes || res.HomeOperacoes || [],
      });
    } catch (err) {
      setError(err.message || 'Não foi possível carregar o gráfico.');
      setItems([]);
      setChartMeta({
        mode: 'por-operacao',
        localLabel: null,
        mineTotal: null,
        localTotal: null,
        partilhaTotal: null,
        homeOperacoes: [],
      });
    } finally {
      setLoading(false);
    }
  }, [ano, periodMes, applied, wantConsultorMode]);

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

  const toggleIn = (list, value, setter) => {
    setter(list.includes(value) ? list.filter(v => v !== value) : [...list, value]);
  };

  const applyFilters = () => {
    setApplied({
      metric,
      mes,
      naturezas: [...naturezas],
      estados: [...estados],
      probabilidades: [...probabilidades],
    });
    setShowFilters(false);
  };

  const clearFilters = () => {
    setMetric(OPERACAO_CHART_DEFAULT_FILTERS.metric);
    setMes(OPERACAO_CHART_DEFAULT_FILTERS.mes);
    setNaturezas([...OPERACAO_CHART_DEFAULT_FILTERS.naturezas]);
    setEstados([...OPERACAO_CHART_DEFAULT_FILTERS.estados]);
    setProbabilidades([...OPERACAO_CHART_DEFAULT_FILTERS.probabilidades]);
    setApplied({
      metric: OPERACAO_CHART_DEFAULT_FILTERS.metric,
      mes: OPERACAO_CHART_DEFAULT_FILTERS.mes,
      naturezas: [...OPERACAO_CHART_DEFAULT_FILTERS.naturezas],
      estados: [...OPERACAO_CHART_DEFAULT_FILTERS.estados],
      probabilidades: [...OPERACAO_CHART_DEFAULT_FILTERS.probabilidades],
    });
    setShowFilters(false);
  };

  const openPopover = useCallback(() => {
    const btn = filterBtnRef.current;
    if (!btn) return;
    const r = btn.getBoundingClientRect();
    const width = 360; // keep in sync with CSS .chart-filter-popover
    const margin = 12;
    const left = Math.max(margin, Math.min(r.left, window.innerWidth - width - margin));
    const top = Math.min(r.bottom + 8, window.innerHeight - 420); // keep visible
    setPopoverPos({ left, top });
    setShowFilters(true);
  }, []);

  const closePopover = useCallback(() => {
    setShowFilters(false);
  }, []);

  useEffect(() => {
    if (!showFilters) return undefined;
    const onDoc = (e) => {
      const t = e.target;
      if (popoverRef.current?.contains(t)) return;
      if (filterBtnRef.current?.contains(t)) return;
      closePopover();
    };
    const onKey = (e) => {
      if (e.key === 'Escape') closePopover();
    };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onKey);
    const onScroll = () => openPopover();
    window.addEventListener('scroll', onScroll, true);
    window.addEventListener('resize', onScroll);
    return () => {
      document.removeEventListener('mousedown', onDoc);
      document.removeEventListener('keydown', onKey);
      window.removeEventListener('scroll', onScroll, true);
      window.removeEventListener('resize', onScroll);
    };
  }, [showFilters, closePopover, openPopover]);

  const homeSet = useMemo(() => {
    const set = new Set();
    for (const h of chartMeta.homeOperacoes || []) {
      if (h) set.add(String(h).trim().toLowerCase());
    }
    if (chartMeta.localLabel) {
      for (const part of String(chartMeta.localLabel).split('·')) {
        const t = part.trim().toLowerCase();
        if (t) set.add(t);
      }
    }
    return set;
  }, [chartMeta.homeOperacoes, chartMeta.localLabel]);

  const isHomeLabel = (label) => homeSet.has(String(label || '').trim().toLowerCase());

  const euLabel = commissionActorLabel(user);
  const modeKey = String(chartMeta.mode || '').toLowerCase();
  const isConsultorChart = modeKey === 'operacoes-com-casa' || modeKey === 'consultor-vs-local';
  const homeLabel = chartMeta.localLabel || (chartMeta.homeOperacoes || [])[0] || null;
  const mineTotal = Number(chartMeta.mineTotal ?? 0);
  const showEu = isConsultorChart && mineTotal > 0;

  const slices = useMemo(() => {
    const colorFor = (label, idx) => operacaoPieColor(label, idx);
    const rows = (items || [])
      .map((it, idx) => {
        const label = it.operacao || it.Operacao || 'Outros';
        const isHome = Boolean(it.isHome ?? it.IsHome) || isHomeLabel(label);
        return {
          key: label || `op-${idx}`,
          label,
          isHome,
          isEu: false,
          total: Number(it.total ?? it.Total ?? 0),
          count: Number(it.count ?? it.Count ?? 0),
          color: colorFor(label, idx),
        };
      });

    const expanded = [];
    let euCarved = false;
    for (const r of rows) {
      if (showEu && !euCarved && r.isHome && r.total > 0) {
        const euAmt = Math.min(mineTotal, r.total);
        const rest = Math.max(0, r.total - euAmt);
        expanded.push({
          key: '__eu__',
          label: euLabel,
          isHome: true,
          isEu: true,
          total: euAmt,
          count: 0,
          color: '#1F2C5C',
        });
        if (rest > 0) {
          expanded.push({ ...r, total: rest });
        }
        euCarved = true;
      } else {
        expanded.push(r);
      }
    }

    const drawable = expanded.filter(r => r.total > 0);
    const sum = drawable.reduce((acc, r) => acc + r.total, 0);
    let angle = 0;
    const pie = drawable.map(r => {
      const sweep = sum > 0 ? (r.total / sum) * 360 : 0;
      const start = angle;
      angle += sweep;
      return { ...r, start, end: angle, pct: sum > 0 ? (r.total / sum) * 100 : 0, sum };
    });
    const legendExtras = expanded
      .filter(r => r.total <= 0)
      .map(r => ({ ...r, start: 0, end: 0, pct: 0, sum }));
    return { pie, legend: [...pie, ...legendExtras], sum };
  }, [items, homeSet, showEu, mineTotal, euLabel]);

  const pieSlices = slices.pie || [];
  const legendSlices = slices.legend || [];
  const grandTotal = slices.sum || 0;
  const localTotal = Number(chartMeta.localTotal ?? grandTotal);
  const partilhaTotal = Number(chartMeta.partilhaTotal ?? 0);
  const soloTotal = Math.max(0, mineTotal - partilhaTotal);
  const mineShare = localTotal > 0 ? (mineTotal / localTotal) * 100 : 0;
  const euSlice = pieSlices.find(s => s.isEu || s.key === '__eu__') || null;
  const euSlicePct = euSlice ? euSlice.pct : (grandTotal > 0 ? (mineTotal / grandTotal) * 100 : 0);
  const activeMetric = OPERACAO_CHART_METRICS.find(m => m.value === applied.metric) || OPERACAO_CHART_METRICS[0];
  const formatValue = (n) => {
    if (applied.metric === 'count') return new Intl.NumberFormat('pt-PT').format(n);
    return formatCurrency(n);
  };
  const filterCount = countAppliedFilters(applied, OPERACAO_CHART_DEFAULT_FILTERS);
  const hovered = pieSlices.find(s => s.key === hoverKey) || legendSlices.find(s => s.key === hoverKey) || null;
  const hoveringEu = hoverKey === '__eu__';
  const chartTitle = isConsultorChart
    ? (homeLabel ? `Operações · ${euLabel} · ${homeLabel}` : `Operações · ${euLabel}`)
    : 'Comissão para gestão';
  const centerLabel = hoveringEu
    ? euLabel
    : hovered
      ? hovered.label
      : (showEu ? euLabel : 'Total');
  const centerValue = hoveringEu
    ? mineTotal
    : hovered
      ? hovered.total
      : (showEu ? mineTotal : grandTotal);
  const centerPct = hoveringEu
    ? euSlicePct
    : hovered
      ? hovered.pct
      : (showEu ? euSlicePct : null);

  const chartSummaryTooltip = (() => {
    const parts = [];
    parts.push(`Total ${formatValue(grandTotal)}`);
    if (isConsultorChart && homeLabel) {
      parts.push(`Loja ${homeLabel} ${formatValue(localTotal)}`);
      if (mineTotal > 0) {
        parts.push(`${euLabel} ${formatValue(mineTotal)}`);
        if (partilhaTotal > 0) {
          parts.push(`solo ${formatValue(soloTotal)}`);
          parts.push(`partilha ${formatValue(partilhaTotal)}`);
        }
        parts.push(`${mineShare.toFixed(0)}% da loja`);
      }
    }
    const effectiveMes = applied.mes || periodMes;
    if (applied.estados?.length) parts.push(applied.estados.join(' · '));
    if (applied.probabilidades?.length) parts.push(`Prob. ${applied.probabilidades.join(' · ')}`);
    if (effectiveMes || ano) parts.push(dashboardPeriodLabel(ano, effectiveMes) || String(ano || ''));
    return parts.join(' · ');
  })();

  return (
    <DashCard
      className={`chart-card operacao-chart-card${showFilters ? ' filters-open' : ''}`}
      icon="chartPie"
      onExplore={onExplore}
      period={dashboardPeriodLabel(ano, applied.mes || periodMes)}
      title={(
        <>
          {chartTitle}
          <span className="operacao-chart-metric-tag">{activeMetric.label}</span>
        </>
      )}
      actions={(
        <div className="operacao-chart-actions">
          <div className="operacao-chart-help-wrap">
            <button
              type="button"
              className="operacao-chart-help-btn"
              aria-label="Detalhes do gráfico"
              aria-describedby="operacao-chart-summary-tip"
            >
              <Icon name="circleQuestion" size={14} />
            </button>
            <div
              id="operacao-chart-summary-tip"
              className="operacao-chart-help-tip"
              role="tooltip"
            >
              {chartSummaryTooltip}
            </div>
          </div>
          {typeof onInspect === 'function' && (
            <button type="button" className="dashboard-inspect-btn" title="Ver filtros e dados (Admin)" aria-label="Ver filtros e dados" onClick={onInspect}>
              <Icon name="eye" size={14} />
            </button>
          )}
          <button
            type="button"
            className={`btn-filter-toggle btn-filter-chart${showFilters ? ' active' : ''}`}
            aria-pressed={showFilters}
            aria-label="Filtros do gráfico"
            title="Filtros"
            ref={filterBtnRef}
            onClick={() => {
              if (showFilters) closePopover();
              else openPopover();
            }}
          >
            <Icon name="filter" size={14} />
            {filterCount > 0 ? <span className="operacao-chart-filter-badge">{filterCount}</span> : null}
          </button>
        </div>
      )}
    >
      <div className="chart-card-body">
        <div className="chart-card-main">
          {loading ? (
            <CardLoading message="A carregar gráfico..." />
          ) : error ? (
            <p className="empty-hint" style={{ padding: '0.75rem' }}>{error}</p>
          ) : pieSlices.length === 0 && legendSlices.length === 0 ? (
            <p className="empty-hint" style={{ padding: '0.75rem' }}>Sem dados para os filtros selecionados.</p>
          ) : (
            <div className="operacao-pie-layout">
              <div className="operacao-pie-visual" aria-hidden={false}>
                <svg viewBox="0 0 200 200" className="operacao-pie-svg" role="img" aria-label={`${chartTitle} — ${activeMetric.label}`}>
                  {pieSlices.map(slice => {
                    const midAngle = (slice.start + slice.end) / 2;
                    const labelPos = polarToCartesian(100, 100, 67, midAngle);
                    const tipPos = polarToCartesian(100, 100, 78, midAngle);
                    const showPctLabel = slice.pct >= 4;
                    const sliceClass = `operacao-pie-slice${slice.isEu ? ' is-eu' : ''}${slice.isHome && !slice.isEu ? ' is-home' : ''}${hoverKey === slice.key ? ' is-hot' : ''}${hoverKey && hoverKey !== slice.key ? ' is-dim' : ''}`;
                    const tipText = `${slice.label}${slice.isEu ? '' : slice.isHome ? ' (a minha)' : ''}: ${formatValue(slice.total)} (${slice.pct < 1 ? slice.pct.toFixed(1) : Math.round(slice.pct)}%)`;
                    return (
                      <g
                        key={slice.key}
                        className={sliceClass}
                        onMouseEnter={() => setHoverKey(slice.key)}
                        onMouseLeave={() => setHoverKey(null)}
                      >
                        <title>{tipText}</title>
                        <path
                          d={describePieSlice(100, 100, 86, slice.start, slice.end)}
                          fill={slice.color}
                        />
                        {showPctLabel ? (
                          <text
                            x={labelPos.x}
                            y={labelPos.y}
                            textAnchor="middle"
                            dominantBaseline="middle"
                            className="operacao-pie-slice-label"
                            pointerEvents="none"
                          >
                            {`${Math.round(slice.pct)}%`}
                          </text>
                        ) : null}
                        {!showPctLabel && hoverKey === slice.key ? (
                          <g className="operacao-pie-slice-tip" pointerEvents="none">
                            <rect
                              x={tipPos.x - 28}
                              y={tipPos.y - 11}
                              width={56}
                              height={20}
                              rx={6}
                              className="operacao-pie-slice-tip-bg"
                            />
                            <text
                              x={tipPos.x}
                              y={tipPos.y + 1}
                              textAnchor="middle"
                              dominantBaseline="middle"
                              className="operacao-pie-slice-tip-text"
                            >
                              {`${slice.pct < 1 ? slice.pct.toFixed(1) : Math.round(slice.pct)}%`}
                            </text>
                          </g>
                        ) : null}
                      </g>
                    );
                  })}
                  <circle cx="100" cy="100" r="48" className="operacao-pie-hole" />
                  <text x="100" y="96" textAnchor="middle" className="operacao-pie-center-label">
                    {centerLabel}
                  </text>
                  <text x="100" y={centerPct != null ? 110 : 114} textAnchor="middle" className="operacao-pie-center-value">
                    {formatValue(centerValue)}
                  </text>
                  {centerPct != null ? (
                    <text x="100" y={124} textAnchor="middle" className="operacao-pie-center-pct">
                      {`${centerPct < 1 ? centerPct.toFixed(1) : Math.round(centerPct)}%`}
                    </text>
                  ) : null}
                </svg>
              </div>
              <ul className="operacao-pie-legend">
                {legendSlices.map(slice => {
                  const pctLabel = `${slice.pct > 0 && slice.pct < 1 ? slice.pct.toFixed(1) : Math.round(slice.pct)}%`;
                  return (
                    <li
                      key={slice.key}
                      className={`${hoverKey === slice.key ? 'is-hot' : ''}${slice.isEu ? ' is-eu' : ''}${slice.isHome && !slice.isEu ? ' is-home' : ''}`.trim()}
                      onMouseEnter={() => setHoverKey(slice.key)}
                      onMouseLeave={() => setHoverKey(null)}
                    >
                      <span className="chart-legend-dot" style={{ background: slice.color }} />
                      <div className="operacao-pie-legend-copy">
                        <div className="operacao-pie-legend-head">
                          <span className="operacao-pie-legend-label">{slice.label}</span>
                          {slice.isEu && homeLabel ? <em className="operacao-pie-home-tag">{homeLabel}</em> : null}
                          {slice.isHome && !slice.isEu ? <em className="operacao-pie-home-tag">A minha</em> : null}
                        </div>
                        <div className="operacao-pie-legend-values">
                          <span className="operacao-pie-legend-amt">{formatValue(slice.total)}</span>
                          <span className="operacao-pie-legend-pct">{pctLabel}</span>
                        </div>
                        {slice.isEu && partilhaTotal > 0 ? (
                          <div className="operacao-pie-legend-split">
                            <span><i>Solo</i> {formatValue(soloTotal)}</span>
                            <span><i>Partilha</i> {formatValue(partilhaTotal)}</span>
                          </div>
                        ) : null}
                      </div>
                    </li>
                  );
                })}
              </ul>
            </div>
          )}
        </div>

        {showFilters && popoverPos && ReactDOM.createPortal(
          <aside
            ref={popoverRef}
            className="chart-filter-popover"
            aria-label="Filtros do gráfico por operação"
            style={{ left: popoverPos.left, top: popoverPos.top }}
          >
            <div className="chart-filter-popover-header">
              <div className="chart-filter-popover-title">Filtro</div>
            </div>
            <div className="chart-filter-popover-body">
              <div className="chart-filter-row">
                <div className="form-group">
                  <label>Métrica</label>
                  <AppSelect
                    value={metric}
                    options={OPERACAO_CHART_METRICS}
                    onChange={setMetric}
                    ariaLabel="Métrica"
                  />
                </div>
                <div className="form-group">
                  <label>Mês</label>
                  <AppSelect
                    value={mes || periodMes || ''}
                    emptyOption="Todos"
                    options={DASH_MONTH_OPTIONS}
                    onChange={setMes}
                    ariaLabel="Mês"
                  />
                </div>
              </div>
              <div className="checkbox-group">
                <span className="checkbox-group-title">Natureza</span>
                <div className="checkbox-group-chips">
                  {NATUREZA_CHOICE_OPTIONS.map(opt => (
                    <label key={opt} className="avaliacao-check">
                      <input
                        type="checkbox"
                        checked={naturezas.includes(opt)}
                        onChange={() => toggleIn(naturezas, opt, setNaturezas)}
                      />
                      {opt}
                    </label>
                  ))}
                </div>
              </div>
              <div className="checkbox-group">
                <span className="checkbox-group-title">Estado</span>
                <div className="checkbox-group-chips">
                  {OPERACAO_CHART_ESTADOS.map(opt => (
                    <label key={opt} className="avaliacao-check">
                      <input
                        type="checkbox"
                        checked={estados.includes(opt)}
                        onChange={() => toggleIn(estados, opt, setEstados)}
                      />
                      {opt}
                    </label>
                  ))}
                </div>
              </div>
              <ProbabilidadeMultiSelect variant="chips" value={probabilidades} onChange={setProbabilidades} />
            </div>
            <div className="chart-filter-popover-actions">
              <button type="button" className="btn btn-primary btn-sm" onClick={applyFilters}>Aplicar</button>
              <button type="button" className="btn btn-secondary btn-sm" onClick={clearFilters}>Limpar</button>
            </div>
          </aside>,
          document.body
        )}
      </div>
    </DashCard>
  );
}

const AVALIACAO_SCALE_ITEMS = [
  { value: '1', label: 'Nunca', hint: 'menos de 20%' },
  { value: '2', label: 'Poucas vezes', hint: 'menos de 40%' },
  { value: '3', label: 'Quase sempre', hint: 'menos de 70%' },
  { value: '4', label: 'Sempre', hint: 'até 100%' },
  { value: '5', label: 'Supera', hint: 'até 130%' },
];

/** Client cache: questionarioId → full team list (filters/pagination applied locally). */
const AVALIACAO_TEAM_CACHE = new Map();

function invalidateAvaliacaoTeamCache(questionarioId) {
  if (questionarioId) AVALIACAO_TEAM_CACHE.delete(questionarioId);
  else AVALIACAO_TEAM_CACHE.clear();
}

async function prefetchAvaliacaoTeam(questionarioId, { force = false } = {}) {
  if (!questionarioId) return { items: [], total: 0 };
  const existing = AVALIACAO_TEAM_CACHE.get(questionarioId);
  // Empty arrays are truthy — only reuse a non-empty warm cache (or any cache if not forcing).
  if (!force && Array.isArray(existing?.items) && (existing.items.length > 0 || existing.loadedAt)) {
    // Allow reuse of empty only briefly (<15s) so a bad first paint can recover via Atualizar / remount.
    if (existing.items.length > 0 || (Date.now() - (existing.loadedAt || 0)) < 15_000)
      return existing;
  }
  if (!force && existing?.promise) return existing.promise;

  const promise = apiFetch(`/api/avaliacao/equipa?questionarioId=${encodeURIComponent(questionarioId)}&page=1&pageSize=500${force ? '&force=true' : ''}`)
    .then(data => {
      const items = data.items || data.Items || [];
      const entry = {
        items,
        total: data.total ?? data.Total ?? items.length,
        loadedAt: Date.now(),
      };
      AVALIACAO_TEAM_CACHE.set(questionarioId, entry);
      return entry;
    })
    .catch(err => {
      const cur = AVALIACAO_TEAM_CACHE.get(questionarioId);
      if (cur?.promise === promise) AVALIACAO_TEAM_CACHE.delete(questionarioId);
      throw err;
    });

  AVALIACAO_TEAM_CACHE.set(questionarioId, { ...(existing || {}), promise });
  return promise;
}

function filterAvaliacaoTeam(items, { search = '', filterAval = false, filterAuto = false, filterNao = false } = {}) {
  const q = String(search || '').trim().toLowerCase();
  return (items || []).filter(m => {
    const email = String(m.email || m.Email || '').toLowerCase();
    const name = String(m.assinatura || m.Assinatura || '').toLowerCase();
    if (q && !email.includes(q) && !name.includes(q)) return false;
    const autoDone = Boolean(m.autoDone ?? m.AutoDone);
    const avalDone = Boolean(m.avaliacaoDone ?? m.AvaliacaoDone);
    if (filterNao) return !avalDone;
    if (filterAval || filterAuto) {
      return (filterAval && avalDone) || (filterAuto && autoDone);
    }
    return true;
  });
}

function AvaliacaoScaleTip() {
  const [open, setOpen] = useState(false);
  const tipRef = useRef(null);

  useEffect(() => {
    if (!open) return undefined;
    const onDoc = (e) => {
      if (tipRef.current && !tipRef.current.contains(e.target)) setOpen(false);
    };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [open]);

  return (
    <span ref={tipRef} className={`avaliacao-tip${open ? ' is-open' : ''}`}>
      <button
        type="button"
        className="avaliacao-tip-btn"
        aria-expanded={open}
        aria-label="Ver escala de avaliação"
        onClick={(e) => { e.stopPropagation(); setOpen(v => !v); }}
      >
        ?
      </button>
      {open && (
        <div className="avaliacao-tip-panel" role="tooltip">
          <strong>Escala de avaliação</strong>
          <ul>
            {AVALIACAO_SCALE_ITEMS.map(item => (
              <li key={item.value}>
                <span className="avaliacao-tip-score">{item.value}</span>
                <span>
                  <em>{item.label}</em>
                  {' — '}
                  {item.hint}
                </span>
              </li>
            ))}
          </ul>
        </div>
      )}
    </span>
  );
}

function AvaliacaoTrimestralCard({ user, actorUser = null }) {
  const [me, setMe] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [open, setOpen] = useState(false);
  const adminActor = actorUser || user;

  const reload = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const data = await apiFetch('/api/avaliacao/me');
      setMe(data);
    } catch (err) {
      setError(err.message || 'Não foi possível carregar a avaliação.');
      setMe(null);
    } finally {
      setLoading(false);
    }
  }, []);

  useEffect(() => { reload(); }, [reload, user?.email, user?.Email]);

  const q = me?.activeQuestionario || me?.ActiveQuestionario;
  const isOpen = Boolean(q?.isOpen ?? q?.IsOpen);
  const days = q?.daysRemaining ?? q?.DaysRemaining;
  const tri = q?.trimestre ?? q?.Trimestre;
  const ano = q?.ano ?? q?.Ano;
  const isManager = Boolean(me?.isManager ?? me?.IsManager) || userIsAdmin(adminActor);
  const autoDone = Boolean(me?.autoavaliacaoDone ?? me?.AutoavaliacaoDone);
  const canAuto = Boolean(me?.canAutoavaliacao ?? me?.CanAutoavaliacao);
  const canFill = isOpen && !autoDone && !isManager;
  const canManage = isManager;
  const canView = isOpen && autoDone && !isManager;
  const canOpenWorkspace = canFill || canManage || canView;
  const sessionIsAdmin = userIsAdmin(adminActor);

  // Warm only the active trimestre — prefetching all Qs flooded SharePoint.
  useEffect(() => {
    if (!me) return;
    if (!canManage) return;
    const active = me.activeQuestionario || me.ActiveQuestionario;
    const id = active?.id || active?.Id;
    if (id) prefetchAvaliacaoTeam(String(id)).catch(() => {});
  }, [me, canManage, user?.email, user?.Email]);

  useEffect(() => {
    const openEvt = () => {
      if (canOpenWorkspace) setOpen(true);
    };
    window.addEventListener('myhabita:open-avaliacao', openEvt);
    return () => window.removeEventListener('myhabita:open-avaliacao', openEvt);
  }, [canOpenWorkspace]);

  let statusLine = 'Sem questionário ativo';
  let statusTone = 'idle';
  if (loading) {
    statusLine = 'A carregar…';
    statusTone = 'loading';
  } else if (q) {
    if (autoDone) {
      statusLine = 'Autoavaliação submetida';
      statusTone = 'done';
    } else if (isOpen) {
      statusLine = days != null ? `Faltam ${days} dia${days === 1 ? '' : 's'}` : 'Período aberto';
      statusTone = days != null && days <= 5 ? 'urgent' : 'open';
    } else {
      statusLine = 'Terminado';
      statusTone = 'closed';
    }
  }

  const periodLabel = q ? `T${tri || '—'} ${ano || ''}`.trim() : null;

  const closeModal = () => {
    setOpen(false);
    reload();
  };
  const overlayDismiss = useOverlayDismiss(closeModal);

  const showFab = !loading && (canManage || canFill);
  if (!showFab && !open) return null;
  const fabLabel = canManage
    ? 'Gerir autoavaliação'
    : (periodLabel ? `Autoavaliação ${periodLabel}` : 'Autoavaliação');

  return (
    <>
      {showFab && (
        <button
          type="button"
          className={`avaliacao-fab tone-${statusTone}`}
          onClick={() => setOpen(true)}
          aria-label={fabLabel}
        >
          <span className="avaliacao-fab-icon" aria-hidden="true">
            <Icon name="clipboardCheck" size={20} color="#FFFFFF" />
          </span>
          <span className="avaliacao-fab-label">{fabLabel}</span>
        </button>
      )}
      {open && ReactDOM.createPortal(
        <div
          className="negocio-modal-overlay avaliacao-modal-overlay"
          {...overlayDismiss}
          role="presentation"
        >
          <div
            className="negocio-modal avaliacao-modal"
            onClick={e => e.stopPropagation()}
            role="dialog"
            aria-modal="true"
            aria-labelledby="avaliacao-modal-title"
          >
            <AvaliacaoWorkspace
              me={me}
              isAdmin={sessionIsAdmin}
              onCollapse={closeModal}
              onRefreshMe={reload}
              initialTab={canManage ? 'gestao' : 'auto'}
              canAuto={canAuto}
              windowOpen={isOpen}
            />
          </div>
        </div>,
        document.body
      )}
    </>
  );
}

function avDateInput(value) {
  if (!value) return '';
  const dt = new Date(value);
  if (Number.isNaN(dt.getTime())) return '';
  return `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}-${String(dt.getDate()).padStart(2, '0')}`;
}

function avPeriodoStatus(q) {
  if (!q) return 'Sem período';
  if (q.blocked ?? q.Blocked) return 'Bloqueado';
  if (q.isOpen ?? q.IsOpen) return 'Aberto';
  const start = q.start || q.Start ? new Date(q.start || q.Start) : null;
  const end = q.end || q.End ? new Date(q.end || q.End) : null;
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  if (start && start > today) return 'Agendado';
  if (end && end < today) return 'Terminado';
  return 'Fechado';
}

function avTodayInput() {
  return avDateInput(new Date());
}

function avCalendarDay(value) {
  if (!value) return null;
  const dt = new Date(value);
  if (Number.isNaN(dt.getTime())) return null;
  return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate()).getTime();
}

function avFormatDay(value) {
  const day = avCalendarDay(value);
  if (day == null) return '';
  return new Date(day).toLocaleDateString('pt-PT');
}

function avTrimestreEndDay(ano, trimestre) {
  const year = Number(avNormalizeAno(ano));
  const tri = Number(avNormalizeTrimestre(trimestre));
  if (!year || tri < 1 || tri > 4) return null;
  return new Date(year, tri * 3, 0).getTime();
}

function avAdmittedAfterPeriod(admission, periodEnd, ano, trimestre) {
  const admitted = avCalendarDay(admission);
  if (admitted == null) return false;
  const quarterEnd = avTrimestreEndDay(ano, trimestre);
  const formEnd = avCalendarDay(periodEnd);
  const cutoff = quarterEnd != null && formEnd != null
    ? Math.min(quarterEnd, formEnd)
    : (quarterEnd ?? formEnd);
  if (cutoff == null) return false;
  return admitted > cutoff;
}

function avNormalizeAno(value) {
  const raw = String(value ?? '').trim();
  const m = raw.match(/(20\d{2}|19\d{2})/);
  return m ? m[1] : raw;
}

function avNormalizeTrimestre(value) {
  const raw = String(value ?? '').trim();
  if (!raw) return '';
  const fromT = raw.replace(/^T\s*/i, '').trim();
  const digit = fromT.match(/[1-4]/);
  return digit ? digit[0] : '';
}

function avPeriodoKey(ano, trimestre) {
  const a = avNormalizeAno(ano);
  const t = avNormalizeTrimestre(trimestre);
  if (!a || !t) return '';
  return `${a}|${t}`;
}

function avPeriodoTakenLabel(status) {
  const s = String(status || '').toLowerCase();
  if (s === 'terminado') return 'terminado';
  if (s === 'aberto') return 'aberto';
  if (s === 'agendado') return 'agendado';
  if (s === 'bloqueado') return 'bloqueado';
  return 'já existe';
}

function avParseQuestionarioPeriodo(q) {
  let ano = q?.ano ?? q?.Ano ?? '';
  let trimestre = q?.trimestre ?? q?.Trimestre ?? '';
  const title = String(q?.title || q?.Title || '').trim();
  const m = title.match(/T?\s*([1-4])\s*[·\-|/ ]\s*(\d{4})/i)
    || title.match(/(\d{4})\s*[·\-|/ ]\s*T?\s*([1-4])/i);
  if (m) {
    if (/^\d{4}$/.test(m[1])) {
      if (!ano) ano = m[1];
      if (!trimestre) trimestre = m[2];
    } else {
      if (!trimestre) trimestre = m[1];
      if (!ano) ano = m[2];
    }
  }
  const key = avPeriodoKey(ano, trimestre);
  if (!key) return null;
  const [a, t] = key.split('|');
  const status = avPeriodoStatus(q);
  return { key, ano: a, trimestre: t, status, q };
}

function avExistingPeriodoMap(questionarios) {
  const map = new Map();
  for (const q of questionarios || []) {
    const parsed = avParseQuestionarioPeriodo(q);
    if (parsed) map.set(parsed.key, parsed);
  }
  return map;
}

function avExistingPeriodoKeys(questionarios) {
  return new Set(avExistingPeriodoMap(questionarios).keys());
}

function emptyAvPeriodoDraft(questionarios = []) {
  const now = new Date();
  const today = avTodayInput();
  const end = new Date(now);
  end.setDate(end.getDate() + 13);
  let ano = now.getFullYear();
  const taken = avExistingPeriodoKeys(questionarios);
  let trimestre = null;
  for (let yearOffset = 0; yearOffset <= 2 && !trimestre; yearOffset += 1) {
    const y = ano + yearOffset;
    const startT = yearOffset === 0 ? Math.floor(now.getMonth() / 3) + 1 : 1;
    for (let t = startT; t <= 4; t += 1) {
      if (!taken.has(avPeriodoKey(y, t))) {
        ano = y;
        trimestre = String(t);
        break;
      }
    }
  }
  return {
    ano: String(ano),
    trimestre: trimestre || '1',
    start: today,
    end: avDateInput(end) < today ? today : avDateInput(end),
    blocked: false,
    addToCalendar: true,
    notify: true,
    creating: true,
  };
}

function draftFromQuestionario(q) {
  const today = avTodayInput();
  const parsed = avParseQuestionarioPeriodo(q);
  const tri = parsed?.trimestre || avNormalizeTrimestre(q?.trimestre || q?.Trimestre) || '1';
  let start = avDateInput(q?.start || q?.Start) || today;
  let end = avDateInput(q?.end || q?.End) || start;
  // Editing still cannot move dates into the past.
  if (start < today) start = today;
  if (end < start) end = start;
  return {
    ano: String(parsed?.ano || q?.ano || q?.Ano || new Date().getFullYear()),
    trimestre: tri,
    start,
    end,
    blocked: Boolean(q?.blocked ?? q?.Blocked),
    addToCalendar: q?.onCalendar ?? q?.OnCalendar ?? true,
    notify: false,
    creating: false,
  };
}

function AvaliacaoWorkspace({ me, isAdmin = false, onCollapse, onRefreshMe, initialTab = 'auto', canAuto = true, windowOpen = false }) {
  const TEAM_PAGE_SIZE = 10;
  const isManager = Boolean(isAdmin || me?.isManager || me?.IsManager);
  const activeQ = me?.activeQuestionario || me?.ActiveQuestionario;
  const [tab, setTab] = useState(initialTab);
  const [page, setPage] = useState(initialTab === 'auto' ? 'autoavaliacao' : initialTab === 'avaliar' ? 'selecioneconsultor' : 'manager');
  const [questionarioId, setQuestionarioId] = useState(activeQ?.id || activeQ?.Id || '');
  const [avaliado, setAvaliado] = useState(null);
  const [team, setTeam] = useState([]);
  const [teamTotal, setTeamTotal] = useState(0);
  const [teamPage, setTeamPage] = useState(1);
  const [teamLoading, setTeamLoading] = useState(false);
  const [searchInput, setSearchInput] = useState('');
  const [search, setSearch] = useState('');
  const [filterAval, setFilterAval] = useState(false);
  const [filterAuto, setFilterAuto] = useState(false);
  const [filterNao, setFilterNao] = useState(false);
  const [perguntas, setPerguntas] = useState([]);
  const [answers, setAnswers] = useState({});
  const [formLoading, setFormLoading] = useState(false);
  const [saving, setSaving] = useState(false);
  const [compare, setCompare] = useState(null);
  const [msg, setMsg] = useState(null);
  const [periodoDraft, setPeriodoDraft] = useState(null);
  const [periodoSaving, setPeriodoSaving] = useState(false);
  const [notifySending, setNotifySending] = useState(false);

  const questionarios = me?.questionarios || me?.Questionarios || [];
  const selectedQ = questionarios.find(q => (q.id || q.Id) === questionarioId) || activeQ;
  const qOpen = Boolean(selectedQ?.isOpen ?? selectedQ?.IsOpen ?? windowOpen);
  const teamPageCount = Math.max(1, Math.ceil(teamTotal / TEAM_PAGE_SIZE) || 1);
  const existingPeriodoMap = useMemo(() => avExistingPeriodoMap(questionarios), [questionarios]);
  const existingPeriodoKeys = useMemo(() => new Set(existingPeriodoMap.keys()), [existingPeriodoMap]);
  const todayStr = avTodayInput();
  const periodoKeyTaken = periodoDraft
    ? existingPeriodoKeys.has(avPeriodoKey(periodoDraft.ano, periodoDraft.trimestre))
      && !(
        !periodoDraft.creating
        && selectedQ
        && avPeriodoKey(selectedQ.ano || selectedQ.Ano, selectedQ.trimestre || selectedQ.Trimestre)
          === avPeriodoKey(periodoDraft.ano, periodoDraft.trimestre)
      )
    : false;

  // If matching was stale and draft landed on a taken trimestre, move to a free one.
  useEffect(() => {
    if (!periodoDraft?.creating) return;
    const key = avPeriodoKey(periodoDraft.ano, periodoDraft.trimestre);
    if (!key || !existingPeriodoKeys.has(key)) return;
    const free = ['1', '2', '3', '4'].find(t => !existingPeriodoKeys.has(avPeriodoKey(periodoDraft.ano, t)));
    if (free && free !== String(periodoDraft.trimestre)) {
      setPeriodoDraft(d => (d ? { ...d, trimestre: free } : d));
    }
  }, [periodoDraft?.creating, periodoDraft?.ano, periodoDraft?.trimestre, existingPeriodoKeys]);

  const switchTab = (next) => {
    setMsg(null);
    setCompare(null);
    setAvaliado(null);
    setSearchInput('');
    setSearch('');
    setTeamPage(1);
    setTab(next);
    if (next === 'auto') setPage('autoavaliacao');
    else if (next === 'avaliar') {
      setFilterNao(true);
      setFilterAval(false);
      setFilterAuto(false);
      setPage('selecioneconsultor');
    } else {
      setFilterNao(false);
      setFilterAval(false);
      setFilterAuto(false);
      setPage('manager');
    }
  };

  const loadTeam = useCallback(async (force = false) => {
    if (!questionarioId) return;
    const cached = AVALIACAO_TEAM_CACHE.get(questionarioId);
    const hasReady = Boolean(cached?.items);
    if (!hasReady || force) setTeamLoading(true);
    setMsg(null);
    try {
      const entry = await prefetchAvaliacaoTeam(questionarioId, { force: Boolean(force) });
      const filtered = filterAvaliacaoTeam(entry.items, { search, filterAval, filterAuto, filterNao });
      const total = filtered.length;
      const start = (teamPage - 1) * TEAM_PAGE_SIZE;
      setTeam(filtered.slice(start, start + TEAM_PAGE_SIZE));
      setTeamTotal(total);
    } catch (err) {
      setMsg(err.message || 'Erro ao carregar equipa.');
      setTeam([]);
      setTeamTotal(0);
    } finally {
      setTeamLoading(false);
    }
  }, [questionarioId, search, filterAval, filterAuto, filterNao, teamPage]);

  const applyTeamSearch = () => {
    const next = searchInput.trim();
    if (next === search && teamPage === 1) {
      loadTeam(false);
      return;
    }
    setTeamPage(1);
    setSearch(next);
  };

  const loadPerguntas = useCallback(async (tipo, email) => {
    if (!questionarioId) return;
    setFormLoading(true);
    setMsg(null);
    try {
      const params = new URLSearchParams({ questionarioId, tipo });
      if (email) params.set('avaliadoEmail', email);
      const data = await apiFetch(`/api/avaliacao/perguntas?${params}`);
      setPerguntas(data.items || data.Items || []);
      setAnswers({});
    } catch (err) {
      setMsg(err.message || 'Erro ao carregar perguntas.');
      setPerguntas([]);
    } finally {
      setFormLoading(false);
    }
  }, [questionarioId]);

  useEffect(() => {
    if (page === 'manager' || page === 'selecioneconsultor') loadTeam();
  }, [page, loadTeam]);

  useEffect(() => {
    if (!qOpen) return;
    if (page === 'autoavaliacao') loadPerguntas('Autoavaliação');
    if (page === 'avaliacao' && avaliado) loadPerguntas('Avaliação', avaliado.email || avaliado.Email);
  }, [page, avaliado, loadPerguntas, qOpen]);

  const setAnswer = (perguntaId, patch) => {
    setAnswers(prev => ({ ...prev, [perguntaId]: { ...(prev[perguntaId] || {}), ...patch } }));
  };

  const requiredCount = perguntas.filter(p => p.obrigatoria ?? p.Obrigatoria).length;
  const answeredRequired = perguntas.filter(p => {
    if (!(p.obrigatoria ?? p.Obrigatoria)) return false;
    const a = answers[p.id || p.Id];
    return a && (a.resposta || a.respostaTexto);
  }).length;

  const submitForm = async (tipo) => {
    if (!qOpen) {
      setMsg('O período de avaliação terminou. Já não é possível preencher o formulário.');
      return;
    }
    if (answeredRequired < requiredCount) {
      setMsg('Responda a todas as perguntas obrigatórias.');
      return;
    }
    setSaving(true);
    setMsg(null);
    try {
      await apiFetch('/api/avaliacao/submit', {
        method: 'POST',
        body: JSON.stringify({
          questionarioId,
          tipo,
          avaliadoEmail: tipo === 'Avaliação' ? (avaliado?.email || avaliado?.Email) : null,
          answers: perguntas.map(p => {
            const id = p.id || p.Id;
            const a = answers[id] || {};
            return { perguntaId: id, resposta: a.resposta || null, respostaTexto: a.respostaTexto || null };
          }).filter(a => a.resposta || a.respostaTexto),
        }),
      });
      setMsg(tipo === 'Autoavaliação' ? 'Autoavaliação submetida!' : 'Avaliação submetida!');
      invalidateAvaliacaoTeamCache(questionarioId);
      if (onRefreshMe) await onRefreshMe();
      prefetchAvaliacaoTeam(questionarioId, { force: true }).catch(() => {});
      setTimeout(() => {
        if (tipo === 'Avaliação') {
          setAvaliado(null);
          setPage('selecioneconsultor');
          setTab('avaliar');
        } else {
          setPage('autoavaliacao');
          setTab('auto');
        }
      }, 700);
    } catch (err) {
      setMsg(err.message || 'Erro ao submeter.');
    } finally {
      setSaving(false);
    }
  };

  const openCompare = async (member) => {
    setAvaliado(member);
    setFormLoading(true);
    setMsg(null);
    try {
      const params = new URLSearchParams({
        email: member.email || member.Email,
        questionarioId,
      });
      const data = await apiFetch(`/api/avaliacao/equipa/comparar?${params}`);
      setCompare(data);
      setPage('managerviewsingle');
    } catch (err) {
      setMsg(err.message || 'Sem dados para comparar.');
    } finally {
      setFormLoading(false);
    }
  };

  const grouped = useMemo(() => {
    const map = new Map();
    for (const p of perguntas) {
      const sub = p.subsecao || p.Subsecao || 'Geral';
      if (!map.has(sub)) map.set(sub, []);
      map.get(sub).push(p);
    }
    return [...map.entries()];
  }, [perguntas]);

  const compareGrouped = useMemo(() => {
    const rows = compare?.rows || compare?.Rows || [];
    const map = new Map();
    for (const r of rows) {
      const sub = r.subsecao || r.Subsecao || 'Geral';
      if (!map.has(sub)) map.set(sub, []);
      map.get(sub).push(r);
    }
    return [...map.entries()];
  }, [compare]);

  const subtitle = (() => {
    if (page === 'avaliacao' && avaliado) return avaliado.assinatura || avaliado.Assinatura;
    if (page === 'managerviewsingle' && avaliado) return `Comparar · ${avaliado.assinatura || avaliado.Assinatura}`;
    const q = selectedQ;
    if (!q) return '';
    return `T${q.trimestre || q.Trimestre || '—'} ${q.ano || q.Ano || ''}`.trim();
  })();

  const savePeriodo = async () => {
    if (!periodoDraft) return;
    if (!periodoDraft.start || !periodoDraft.end) {
      setMsg('Indique as datas de início e fim.');
      return;
    }
    if (periodoDraft.start < todayStr || periodoDraft.end < todayStr) {
      setMsg('As datas de início e fim só podem ser hoje ou no futuro.');
      return;
    }
    if (periodoDraft.end < periodoDraft.start) {
      setMsg('A data de fim tem de ser igual ou posterior ao início.');
      return;
    }
    if (periodoKeyTaken) {
      setMsg(`Já existe autoavaliação para T${periodoDraft.trimestre} ${periodoDraft.ano}.`);
      return;
    }
    setPeriodoSaving(true);
    setMsg(null);
    try {
      const body = {
        ano: periodoDraft.ano,
        trimestre: periodoDraft.trimestre,
        start: `${periodoDraft.start}T08:00:00`,
        end: `${periodoDraft.end}T18:00:00`,
        blocked: !!periodoDraft.blocked,
        addToCalendar: !!periodoDraft.addToCalendar,
      };
      const saved = periodoDraft.creating
        ? await apiFetch('/api/avaliacao/periodos', { method: 'POST', body: JSON.stringify(body) })
        : await apiFetch(`/api/avaliacao/periodos/${encodeURIComponent(questionarioId)}`, { method: 'PUT', body: JSON.stringify(body) });
      const savedId = saved?.id || saved?.Id;
      if (savedId) setQuestionarioId(savedId);
      if (onRefreshMe) await onRefreshMe();
      if (periodoDraft.notify && !periodoDraft.blocked) {
        try {
          const params = new URLSearchParams();
          if (savedId) params.set('questionarioId', savedId);
          const result = await apiFetch(`/api/notificacoes/avaliacao/lembretes?${params}`, { method: 'POST' });
          const sent = result?.sent ?? result?.Sent ?? 0;
          setMsg(`Período guardado. Notificações: ${sent} enviada${sent === 1 ? '' : 's'}.`);
        } catch (err) {
          setMsg(`Período guardado. Notificações: ${err.message || 'não foi possível enviar.'}`);
        }
      } else {
        setMsg(periodoDraft.creating ? 'Nova autoavaliação criada.' : 'Período atualizado.');
      }
      setPeriodoDraft(null);
    } catch (err) {
      setMsg(err.message || 'Não foi possível guardar o período.');
    } finally {
      setPeriodoSaving(false);
    }
  };

  const sendPeriodoNotify = async () => {
    if (!questionarioId) return;
    setNotifySending(true);
    setMsg(null);
    try {
      const result = await apiFetch(`/api/notificacoes/avaliacao/lembretes?questionarioId=${encodeURIComponent(questionarioId)}`, { method: 'POST' });
      const sent = result?.sent ?? result?.Sent ?? 0;
      const failed = result?.failed ?? result?.Failed ?? 0;
      setMsg(sent || failed
        ? `Notificações: ${sent} enviada${sent === 1 ? '' : 's'}${failed ? ` · ${failed} falha${failed === 1 ? '' : 's'}` : ''}.`
        : (result?.errors?.[0] || result?.Errors?.[0] || 'Sem destinatários pendentes.'));
    } catch (err) {
      setMsg(err.message || 'Não foi possível enviar notificações.');
    } finally {
      setNotifySending(false);
    }
  };

  return (
    <div className="negocio-modal-panel avaliacao-workspace">
      <div className="negocio-modal-header">
        <div>
          <h3 id="avaliacao-modal-title">Avaliação trimestral</h3>
          {subtitle && <p className="negocio-modal-subtitle">{subtitle}</p>}
        </div>
        <button type="button" className="negocio-modal-close" onClick={onCollapse} aria-label="Fechar">
          <Icon name="close" size={16} color="#FFFFFF" />
        </button>
      </div>

      {isManager ? (
        <div className="avaliacao-tabs-bar">
          <div className="tabs tabs-segmented avaliacao-tabs" role="tablist" aria-label="Modos de avaliação">
            <button
              type="button"
              className={`tab ${tab === 'auto' ? 'active' : ''}`}
              role="tab"
              aria-selected={tab === 'auto'}
              disabled={(!canAuto || !qOpen) && tab !== 'auto'}
              onClick={() => switchTab('auto')}
            >
              Autoavaliação
            </button>
            <button
              type="button"
              className={`tab ${tab === 'gestao' ? 'active' : ''}`}
              role="tab"
              aria-selected={tab === 'gestao'}
              onClick={() => switchTab('gestao')}
            >
              Gestão
            </button>
            <button
              type="button"
              className={`tab ${tab === 'avaliar' ? 'active' : ''}`}
              role="tab"
              aria-selected={tab === 'avaliar'}
              disabled={!qOpen && tab !== 'avaliar'}
              onClick={() => switchTab('avaliar')}
            >
              Avaliar
            </button>
          </div>
        </div>
      ) : null}

      <div className="negocio-modal-body avaliacao-workspace-body">
        {msg && <p className="avaliacao-float-msg">{msg}</p>}

        {page === 'manager' && isAdmin && (
          <div className="avaliacao-periodo">
            <div className="avaliacao-periodo-head">
              <div>
                <strong>Período de autoavaliação</strong>
                {selectedQ && (
                  <p>
                    T{String(selectedQ.trimestre || selectedQ.Trimestre || '').replace(/^T/i, '')} {selectedQ.ano || selectedQ.Ano}
                    {' · '}
                    <span className={`avaliacao-periodo-status is-${avPeriodoStatus(selectedQ).toLowerCase()}`}>{avPeriodoStatus(selectedQ)}</span>
                    {(selectedQ.start || selectedQ.Start) && (selectedQ.end || selectedQ.End) && (
                      <> · {new Date(selectedQ.start || selectedQ.Start).toLocaleDateString('pt-PT')} – {new Date(selectedQ.end || selectedQ.End).toLocaleDateString('pt-PT')}</>
                    )}
                    {(selectedQ.onCalendar ?? selectedQ.OnCalendar) ? ' · No calendário' : ''}
                  </p>
                )}
              </div>
              <div className="avaliacao-periodo-actions">
                <button type="button" className="btn btn-secondary btn-sm" onClick={() => setPeriodoDraft(emptyAvPeriodoDraft(questionarios))}>
                  Nova autoavaliação
                </button>
                {selectedQ && (
                  <button type="button" className="btn btn-secondary btn-sm" onClick={() => setPeriodoDraft(draftFromQuestionario(selectedQ))}>
                    Editar datas
                  </button>
                )}
                {selectedQ && (selectedQ.isOpen ?? selectedQ.IsOpen) && (
                  <button type="button" className="btn btn-primary btn-sm" disabled={notifySending} onClick={sendPeriodoNotify}>
                    {notifySending ? 'A enviar…' : 'Enviar notificações'}
                  </button>
                )}
              </div>
            </div>
            {periodoDraft && (
              <div className="avaliacao-periodo-form">
                <div className="chart-filter-row">
                  <div className="form-group">
                    <label>Ano</label>
                    <input
                      type="number"
                      min="2024"
                      max="2035"
                      value={periodoDraft.ano}
                      onChange={e => {
                        const ano = e.target.value;
                        setPeriodoDraft(d => {
                          const next = { ...d, ano };
                          if (d.creating) {
                            const taken = existingPeriodoKeys;
                            let tri = String(d.trimestre || '1');
                            if (taken.has(avPeriodoKey(ano, tri))) {
                              tri = ['1', '2', '3', '4'].find(t => !taken.has(avPeriodoKey(ano, t))) || tri;
                            }
                            next.trimestre = tri;
                          }
                          return next;
                        });
                      }}
                    />
                  </div>
                  <div className="form-group">
                    <label>Trimestre</label>
                    <AppSelect
                      value={periodoDraft.trimestre}
                      options={['1', '2', '3', '4'].map(t => {
                        const key = avPeriodoKey(periodoDraft.ano, t);
                        const existing = existingPeriodoMap.get(key);
                        const isCurrentEdit = !periodoDraft.creating
                          && selectedQ
                          && avPeriodoKey(selectedQ.ano || selectedQ.Ano, selectedQ.trimestre || selectedQ.Trimestre) === key;
                        const taken = Boolean(existing) && !isCurrentEdit;
                        return {
                          value: t,
                          label: taken ? `T${t} (${avPeriodoTakenLabel(existing?.status)})` : `T${t}`,
                          disabled: taken,
                        };
                      })}
                      onChange={v => setPeriodoDraft(d => ({ ...d, trimestre: v }))}
                      ariaLabel="Trimestre"
                    />
                  </div>
                </div>
                <div className="chart-filter-row">
                  <div className="form-group">
                    <label>Data início</label>
                    <DatePickerField
                      min={todayStr}
                      value={periodoDraft.start}
                      onChange={(startRaw) => {
                        const start = startRaw < todayStr ? todayStr : startRaw;
                        setPeriodoDraft(d => ({
                          ...d,
                          start,
                          end: !d.end || d.end < start ? start : d.end,
                        }));
                      }}
                    />
                  </div>
                  <div className="form-group">
                    <label>Data fim</label>
                    <DatePickerField
                      min={periodoDraft.start && periodoDraft.start > todayStr ? periodoDraft.start : todayStr}
                      value={periodoDraft.end}
                      onChange={(endRaw) => {
                        const floor = periodoDraft.start && periodoDraft.start > todayStr ? periodoDraft.start : todayStr;
                        const end = endRaw < floor ? floor : endRaw;
                        setPeriodoDraft(d => ({ ...d, end }));
                      }}
                    />
                  </div>
                </div>
                {periodoKeyTaken && (
                  <p className="form-hint" style={{ color: '#b42318' }}>
                    Já existe uma autoavaliação para T{periodoDraft.trimestre} {periodoDraft.ano}. Escolha outro trimestre ou ano.
                  </p>
                )}
                <div className="avaliacao-periodo-checks">
                  <label className="avaliacao-check">
                    <input
                      type="checkbox"
                      checked={!!periodoDraft.blocked}
                      onChange={e => setPeriodoDraft(d => ({ ...d, blocked: e.target.checked }))}
                    />
                    Bloquear período
                  </label>
                  <label className="avaliacao-check">
                    <input
                      type="checkbox"
                      checked={!!periodoDraft.addToCalendar}
                      onChange={e => setPeriodoDraft(d => ({ ...d, addToCalendar: e.target.checked }))}
                    />
                    Adicionar no calendário
                  </label>
                  <label className="avaliacao-check">
                    <input
                      type="checkbox"
                      checked={!!periodoDraft.notify}
                      onChange={e => setPeriodoDraft(d => ({ ...d, notify: e.target.checked }))}
                    />
                    Enviar notificação
                  </label>
                </div>
                <p className="form-hint">O evento contínuo aparece na vista Mensal. Bloquear impede o preenchimento mesmo dentro das datas. Datas só a partir de hoje.</p>
                <div className="avaliacao-periodo-form-actions">
                  <button
                    type="button"
                    className="btn btn-primary btn-sm"
                    disabled={periodoSaving || periodoKeyTaken}
                    onClick={savePeriodo}
                  >
                    {periodoSaving ? 'A guardar…' : (periodoDraft.creating ? 'Iniciar autoavaliação' : 'Guardar período')}
                  </button>
                  <button type="button" className="btn btn-secondary btn-sm" disabled={periodoSaving} onClick={() => setPeriodoDraft(null)}>
                    Cancelar
                  </button>
                </div>
              </div>
            )}
          </div>
        )}

        {(page === 'selecioneconsultor' || page === 'manager') && (
          <div className="avaliacao-team-panel">
            <div className="avaliacao-team-tools">
              <input
                type="search"
                placeholder="Pesquisar colaborador"
                value={searchInput}
                onChange={e => setSearchInput(e.target.value)}
                onKeyDown={e => { if (e.key === 'Enter') applyTeamSearch(); }}
              />
              {page === 'manager' && (
                <AppSelect
                  variant="toolbar"
                  value={questionarioId}
                  options={questionarios.map(q => ({
                    value: q.id || q.Id,
                    label: `T${q.trimestre || q.Trimestre} ${q.ano || q.Ano}`,
                  }))}
                  onChange={v => { setTeamPage(1); setQuestionarioId(v); }}
                  ariaLabel="Questionário"
                />
              )}
              {page === 'selecioneconsultor' ? (
                <button
                  type="button"
                  className={`dash-prob-chip${filterNao ? ' active' : ''}`}
                  onClick={() => { setTeamPage(1); setFilterNao(v => !v); }}
                >
                  Não avaliado
                </button>
              ) : (
                <>
                  <button
                    type="button"
                    className={`dash-prob-chip${filterAval ? ' active' : ''}`}
                    onClick={() => { setTeamPage(1); setFilterAval(v => !v); }}
                  >
                    Avaliação preenchida
                  </button>
                  <button
                    type="button"
                    className={`dash-prob-chip${filterAuto ? ' active' : ''}`}
                    onClick={() => { setTeamPage(1); setFilterAuto(v => !v); }}
                  >
                    Autoavaliação preenchida
                  </button>
                </>
              )}
              <button type="button" className="btn btn-secondary btn-sm" onClick={applyTeamSearch}>
                Pesquisar
              </button>
              <button type="button" className="btn btn-primary btn-sm" onClick={() => loadTeam(true)} disabled={teamLoading}>
                {teamLoading ? 'A carregar…' : 'Atualizar'}
              </button>
            </div>

            <div className="avaliacao-team-scroll">
              {teamLoading && <CardLoading message="A carregar equipa…" />}
              {!teamLoading && team.length === 0 && <p className="empty-hint">Sem colaboradores.</p>}
              {!teamLoading && team.map(m => {
                const email = m.email || m.Email;
                const name = m.assinatura || m.Assinatura;
                const autoDone = Boolean(m.autoDone ?? m.AutoDone);
                const avalDone = Boolean(m.avaliacaoDone ?? m.AvaliacaoDone);
                const autoMedia = m.mediaAuto ?? m.MediaAuto;
                const avalMedia = m.mediaAvaliacao ?? m.MediaAvaliacao;
                const admissao = m.dataAdmissao || m.DataAdmissao;
                const admissaoLabel = avFormatDay(admissao);
                const periodEnd = selectedQ?.end || selectedQ?.End;
                const notAdmitted = avAdmittedAfterPeriod(
                  admissao,
                  periodEnd,
                  selectedQ?.ano || selectedQ?.Ano,
                  selectedQ?.trimestre || selectedQ?.Trimestre,
                );
                return (
                  <button
                    type="button"
                    key={m.id || email}
                    className={`avaliacao-team-row${notAdmitted ? ' is-not-admitted' : ''}${avalDone || autoDone ? ' is-done' : ''}`}
                    onClick={() => {
                      if (notAdmitted && !autoDone && !avalDone) {
                        setMsg(`${name} não estava na equipa neste período${admissaoLabel ? ` (admissão ${admissaoLabel})` : ''}.`);
                        return;
                      }
                      if (page === 'selecioneconsultor') {
                        if (!qOpen) {
                          setMsg('O período de avaliação terminou. Já não é possível avaliar.');
                          return;
                        }
                        if (avalDone) {
                          setMsg(`Avaliação de ${name} já foi submetida!`);
                          return;
                        }
                        setAvaliado(m);
                        setPage('avaliacao');
                      } else {
                        openCompare(m);
                      }
                    }}
                  >
                    <div className="avaliacao-team-who">
                      <span className="avaliacao-team-avatar" aria-hidden="true">
                        <MemberAvatar
                          member={{ email, assinatura: name, id: m.id || m.Id }}
                          size={32}
                        />
                      </span>
                      <div>
                        <strong>{name}</strong>
                        <span>{email}</span>
                        {admissaoLabel && <span className="avaliacao-team-admissao">Admissão {admissaoLabel}</span>}
                      </div>
                    </div>
                    {page === 'manager' && (
                      <div className="avaliacao-medias">
                        {notAdmitted && !autoDone && !avalDone ? (
                          <AppTooltip label={admissaoLabel
                            ? `Não estava na equipa neste período (admissão ${admissaoLabel}).`
                            : 'Não estava na equipa neste período.'}>
                            <span className="avaliacao-status-pill is-na">Não estava na equipa</span>
                          </AppTooltip>
                        ) : (
                          <>
                            <span className={`avaliacao-status-pill${autoDone ? ' is-on' : ''}`}>
                              Auto {autoMedia != null ? Number(autoMedia).toFixed(1) : (autoDone ? 'feita' : 'pendente')}
                            </span>
                            <span className={`avaliacao-status-pill${avalDone ? ' is-on' : ''}`}>
                              Aval. {avalMedia != null ? Number(avalMedia).toFixed(1) : (avalDone ? 'feita' : 'pendente')}
                            </span>
                          </>
                        )}
                      </div>
                    )}
                    {page === 'selecioneconsultor' && avalDone && (
                      <span className="avaliacao-status-pill is-on">Preenchida</span>
                    )}
                  </button>
                );
              })}
            </div>

            {!teamLoading && teamTotal > 0 && (
              <div className="pagination avaliacao-team-pagination">
                <span>
                  {teamTotal} colaborador{teamTotal === 1 ? '' : 'es'}
                  {' · '}
                  Página {teamPage} / {teamPageCount}
                </span>
                <span className="avaliacao-team-pager">
                  <AppTooltip label="Página anterior">
                    <button
                      type="button"
                      className="btn btn-secondary btn-icon"
                      disabled={teamPage <= 1 || teamLoading}
                      onClick={() => setTeamPage(p => Math.max(1, p - 1))}
                      aria-label="Página anterior"
                    >
                      <Icon name="arrowLeft" size={14} />
                    </button>
                  </AppTooltip>
                  <AppTooltip label="Página seguinte">
                    <button
                      type="button"
                      className="btn btn-secondary btn-icon"
                      disabled={teamPage >= teamPageCount || teamLoading}
                      onClick={() => setTeamPage(p => Math.min(teamPageCount, p + 1))}
                      aria-label="Página seguinte"
                    >
                      <Icon name="arrowRight" size={14} />
                    </button>
                  </AppTooltip>
                </span>
              </div>
            )}
          </div>
        )}

        {(page === 'autoavaliacao' || page === 'avaliacao') && !qOpen && (
          <div className="avaliacao-closed-panel">
            {page === 'avaliacao' && (
              <button type="button" className="btn btn-secondary btn-sm avaliacao-back-btn" onClick={() => { setAvaliado(null); setPage('selecioneconsultor'); }}>
                ← Voltar à lista
              </button>
            )}
            <p className="empty-hint">Este período de avaliação terminou. Já não é possível preencher o formulário.</p>
          </div>
        )}

        {(page === 'autoavaliacao' || page === 'avaliacao') && qOpen && (
          <div className="avaliacao-form">
            {page === 'avaliacao' && (
              <button type="button" className="btn btn-secondary btn-sm avaliacao-back-btn" onClick={() => { setAvaliado(null); setPage('selecioneconsultor'); }}>
                ← Voltar à lista
              </button>
            )}
            {formLoading && <CardLoading message="A carregar perguntas…" />}
            {!formLoading && grouped.length === 0 && <p className="empty-hint">Sem perguntas para este perfil.</p>}
            {!formLoading && grouped.map(([sub, items]) => (
              <section key={sub} className="avaliacao-section">
                <header>{sub}</header>
                {items.map(p => {
                  const id = p.id || p.Id;
                  const tipo = p.tipoResposta || p.TipoResposta || '';
                  const isScale = /classifica/i.test(tipo);
                  const a = answers[id] || {};
                  return (
                    <div key={id} className="avaliacao-question">
                      <div className="avaliacao-question-head">
                        <strong>{p.ordem ?? p.Ordem}.</strong>
                        <span>{p.texto || p.Texto}</span>
                        {isScale && <AvaliacaoScaleTip />}
                      </div>
                      {isScale ? (
                        <div className="avaliacao-scale" role="radiogroup" aria-label="Classificação 1 a 5">
                          {AVALIACAO_SCALE_ITEMS.map(item => (
                            <button
                              key={item.value}
                              type="button"
                              className={`avaliacao-scale-btn${a.resposta === item.value ? ' active' : ''}`}
                              aria-pressed={a.resposta === item.value}
                              aria-label={`${item.value} — ${item.label} (${item.hint})`}
                              onClick={() => setAnswer(id, { resposta: item.value, respostaTexto: null })}
                            >
                              <span className="avaliacao-scale-num">{item.value}</span>
                              <span className="avaliacao-scale-label">{item.label}</span>
                            </button>
                          ))}
                        </div>
                      ) : (
                        <textarea
                          rows={3}
                          placeholder="Escreva a sua resposta…"
                          value={a.respostaTexto || ''}
                          onChange={e => setAnswer(id, { respostaTexto: e.target.value, resposta: null })}
                        />
                      )}
                    </div>
                  );
                })}
              </section>
            ))}
          </div>
        )}

        {page === 'managerviewsingle' && (
          <div className="avaliacao-compare">
            <button type="button" className="btn btn-secondary btn-sm avaliacao-back-btn" onClick={() => { setCompare(null); setPage('manager'); }}>
              ← Voltar à gestão
            </button>
            <div className="avaliacao-compare-legend">
              <span className="chip colab">Colaborador</span>
              <span className="chip dir">Direção</span>
            </div>
            {formLoading && <CardLoading />}
            {!formLoading && compareGrouped.map(([sub, rows]) => (
              <section key={sub} className="avaliacao-section">
                <header>{sub}</header>
                {rows.map(r => (
                  <div key={r.perguntaId || r.PerguntaId} className="avaliacao-compare-row">
                    <div className="avaliacao-compare-q">{(r.ordem ?? r.Ordem)}. {(r.texto || r.Texto)}</div>
                    <div className="avaliacao-compare-vals">
                      <span className="chip colab">{r.valorColaborador || r.ValorColaborador || r.textoColaborador || r.TextoColaborador || '—'}</span>
                      <span className="chip dir">{r.valorDirecao || r.ValorDirecao || r.textoDirecao || r.TextoDirecao || '—'}</span>
                    </div>
                  </div>
                ))}
              </section>
            ))}
          </div>
        )}
      </div>

      {(page === 'autoavaliacao' || page === 'avaliacao') && qOpen && (
        <footer className="avaliacao-workspace-foot">
          <span className="avaliacao-req">Obrigatório {answeredRequired}/{requiredCount}</span>
          <button type="button" className="btn btn-secondary" onClick={onCollapse}>Fechar</button>
          <button
            type="button"
            className="btn btn-primary"
            disabled={saving || !qOpen}
            onClick={() => submitForm(page === 'autoavaliacao' ? 'Autoavaliação' : 'Avaliação')}
          >
            {saving ? 'A submeter…' : 'Submeter'}
          </button>
        </footer>
      )}
    </div>
  );
}

const DASHBOARD_GRID_COLS = 12;
const DASHBOARD_ROW_HEIGHT = 36;
const DASHBOARD_LAYOUT_VERSION = 4;

const DASHBOARD_WIDGET_DEFS = {
  'kpi-pipeline': { label: 'Negócios em Pipeline', minW: 2, minH: 4, defaultW: 3, defaultH: 4, kpiIndex: 0 },
  'kpi-valor': { label: 'Valor total faturação', minW: 3, minH: 6, defaultW: 7, defaultH: 8, requiresMediacao: true },
  'kpi-ganhos': { label: 'Negócios Ganhos', minW: 2, minH: 4, defaultW: 3, defaultH: 4, kpiIndex: 2 },
  'kpi-comissao': { label: 'Comissão Estimada', minW: 2, minH: 4, defaultW: 3, defaultH: 4, kpiIndex: 3 },
  'kpi-comissao-forecast': { label: 'Previsão Comissão Habita', minW: 3, minH: 6, defaultW: 5, defaultH: 6, requiresMediacao: true },
  'kpi-pipeline-alto': { label: 'Pipeline Em Curso Alto', minW: 2, minH: 4, defaultW: 3, defaultH: 4, requiresMediacao: true },
  'kpi-angariacao': { label: 'Angariações', minW: 3, minH: 4, defaultW: 4, defaultH: 6, requiresAngariacao: true },
  'kpi-intermediacao': { label: 'Intermediação Crédito', minW: 3, minH: 4, defaultW: 4, defaultH: 6, requiresIntermediacao: true },
  'kpi-mix-natureza': { label: 'Mix Vendas / Arrendamento', minW: 3, minH: 4, defaultW: 4, defaultH: 6, requiresMediacao: true },
  pipeline: { label: 'Pipeline', minW: 4, minH: 8, defaultW: 8, defaultH: 12, requiresMediacao: true },
  progress: { label: 'Comissão', minW: 3, minH: 6, defaultW: 4, defaultH: 6, requiresCommission: true },
  comissao: { label: 'Comissão', minW: 3, minH: 6, defaultW: 4, defaultH: 6, requiresCommission: true },
  challenges: { label: 'Desafios do Mês', minW: 3, minH: 4, defaultW: 4, defaultH: 4, requiresMediacao: true, requiresConsultor: true },
  leaderboard: { label: 'Ranking', minW: 3, minH: 4, defaultW: 4, defaultH: 4, requiresMediacao: true },
  'operacao-chart': { label: 'Comissão para gestão', minW: 3, minH: 6, defaultW: 4, defaultH: 8, requiresMediacao: true },
  'activities-today': { label: 'Atividades da Semana', minW: 3, minH: 4, defaultW: 4, defaultH: 6, requiresMediacao: true },
  'activities-recent': { label: 'Atividade Recente', minW: 3, minH: 4, defaultW: 4, defaultH: 6, requiresMediacao: true },
  soi: { label: 'SOI', minW: 3, minH: 4, defaultW: 4, defaultH: 6, requiresSoi: true },
  'avaliacao-trimestral': { label: 'Autoavaliação', minW: 2, minH: 4, defaultW: 3, defaultH: 4, phoneH: 8, pinned: true },
};

function dashboardLayoutStorageKey(userKey) {
  const slug = (userKey || 'default').toLowerCase().replace(/[^a-z0-9@._-]+/g, '-');
  return `myhabita.dashboard.layout.${slug}`;
}

function loadDashboardLayout(userKey) {
  try {
    const raw = window.localStorage.getItem(dashboardLayoutStorageKey(userKey));
    return raw ? JSON.parse(raw) : null;
  } catch {
    return null;
  }
}

function saveDashboardLayout(userKey, layout) {
  try {
    window.localStorage.setItem(dashboardLayoutStorageKey(userKey), JSON.stringify({
      ...(layout || {}),
      v: DASHBOARD_LAYOUT_VERSION,
    }));
  } catch { /* ignore quota */ }
}

function loadDashboardDefaultLayout(ctx) {
  // Global template stored under the "default" localStorage key (cached from API).
  // Admin can overwrite it; other users will restore from it.
  try {
    const raw = window.localStorage.getItem(dashboardLayoutStorageKey('default'));
    const saved = raw ? JSON.parse(raw) : null;
    if (saved?.widgets?.length) return normalizeDashboardLayout(saved, ctx);
  } catch { /* ignore */ }
  return buildDefaultDashboardLayout(ctx);
}

async function fetchDashboardDefaultLayout(ctx) {
  try {
    const data = await apiFetch('/api/dashboard/default-layout');
    const widgets = data.widgets || data.Widgets || [];
    if (Array.isArray(widgets) && widgets.length) {
      const layout = { widgets, v: DASHBOARD_LAYOUT_VERSION, savedAt: data.savedAt || data.SavedAt || null };
      saveDashboardLayout('default', layout);
      return normalizeDashboardLayout(layout, ctx);
    }
  } catch { /* fall through */ }
  return loadDashboardDefaultLayout(ctx);
}

function dashboardLayoutSyncedAtKey(userKey) {
  const slug = (userKey || 'default').toLowerCase().replace(/[^a-z0-9@._-]+/g, '-');
  return `myhabita.dashboard.syncedAt.${slug}`;
}

function dashboardContext({ hasMediacao, hasSoi, hasAngariacao, hasIntermediacao, user }) {
  return {
    hasMediacao,
    hasSoi,
    hasAngariacao: !!hasAngariacao,
    hasIntermediacao: !!hasIntermediacao,
    hasCommission: userHasCommissionProgress(user),
    hasConsultor: userIsConsultor(user),
    // Admins need the commission card in the catalog to place it in the default layout.
    isAdmin: userIsAdmin(user),
  };
}

function dashboardWidgetAvailable(id, ctx) {
  const def = DASHBOARD_WIDGET_DEFS[id];
  if (!def) return false;
  if (def.requiresMediacao && !ctx.hasMediacao) return false;
  if (def.requiresSoi && !ctx.hasSoi) return false;
  if (def.requiresAngariacao && !ctx.hasAngariacao) return false;
  if (def.requiresIntermediacao && !ctx.hasIntermediacao) return false;
  if (def.requiresCommission && !ctx.hasCommission && !ctx.isAdmin) return false;
  if (def.requiresConsultor && !ctx.hasConsultor) return false;
  return true;
}

function getAvailableDashboardWidgetIds(ctx) {
  return Object.keys(DASHBOARD_WIDGET_DEFS).filter(id => {
    // Legacy alias of `comissao` — keep for layout migration, hide from Adicionar.
    if (id === 'progress' || id === 'avaliacao-trimestral') return false;
    return dashboardWidgetAvailable(id, ctx);
  });
}

function clampDashboardWidget(widget) {
  const def = DASHBOARD_WIDGET_DEFS[widget.id] || { minW: 2, minH: 2 };
  const w = Math.max(def.minW, Math.min(DASHBOARD_GRID_COLS, widget.w || def.defaultW || 3));
  const h = Math.max(def.minH, widget.h || def.defaultH || 2);
  const x = Math.max(0, Math.min(DASHBOARD_GRID_COLS - w, widget.x || 0));
  const y = Math.max(0, widget.y || 0);
  const phoneOrder = readWidgetPhoneOrder(widget);
  const phoneH = readWidgetPhoneH(widget);
  const next = { ...widget, x, y, w, h };
  if (phoneOrder != null) next.phoneOrder = phoneOrder;
  if (phoneH != null) next.phoneH = clampPhoneWidgetH(widget.id, phoneH);
  return next;
}

const PHONE_WIDGET_MAX_H = 24;

function widgetPhoneMinRows(def) {
  return def?.phoneH || Math.max(def?.minH || 2, def?.defaultH || 2);
}

function readWidgetPhoneH(widget) {
  const v = widget?.phoneH ?? widget?.PhoneH;
  return Number.isFinite(Number(v)) && Number(v) > 0 ? Number(v) : null;
}

function clampPhoneWidgetH(id, h) {
  const def = DASHBOARD_WIDGET_DEFS[id] || { minH: 2 };
  const min = widgetPhoneMinRows(def);
  const n = Number(h);
  return Math.max(min, Math.min(PHONE_WIDGET_MAX_H, Number.isFinite(n) ? n : min));
}

function resolvePhoneWidgetH(widget) {
  const saved = readWidgetPhoneH(widget);
  if (saved != null) return clampPhoneWidgetH(widget.id, saved);
  return widgetPhoneMinRows(DASHBOARD_WIDGET_DEFS[widget.id] || {});
}

function readWidgetPhoneOrder(widget) {
  const v = widget?.phoneOrder ?? widget?.PhoneOrder;
  return Number.isFinite(v) ? v : null;
}

function serializeDashboardWidgets(widgets) {
  return (widgets || []).map(w => {
    const item = {
      id: w.id,
      x: w.x || 0,
      y: w.y || 0,
      w: w.w || 3,
      h: w.h || 2,
    };
    const phoneOrder = readWidgetPhoneOrder(w);
    if (phoneOrder != null) item.phoneOrder = phoneOrder;
    const phoneH = readWidgetPhoneH(w);
    if (phoneH != null) item.phoneH = phoneH;
    return item;
  });
}

function sortWidgetsForPhone(widgets) {
  const list = [...(widgets || [])];
  const hasOrder = list.some(w => readWidgetPhoneOrder(w) != null);
  if (hasOrder) {
    return list.sort((a, b) => {
      const ao = readWidgetPhoneOrder(a);
      const bo = readWidgetPhoneOrder(b);
      const av = ao == null ? 9999 : ao;
      const bv = bo == null ? 9999 : bo;
      return (av - bv) || (a.y - b.y) || (a.x - b.x) || String(a.id).localeCompare(String(b.id));
    });
  }
  return list.sort((a, b) => (a.y - b.y) || (a.x - b.x) || String(a.id).localeCompare(String(b.id)));
}

function assignPhoneOrder(widgets) {
  return sortWidgetsForPhone(widgets).map((w, i) => ({ ...w, phoneOrder: i }));
}

function reorderPhoneWidgets(widgets, fromId, toIndex) {
  const ordered = assignPhoneOrder(widgets);
  const from = ordered.findIndex(w => w.id === fromId);
  if (from < 0) return widgets;
  const clamped = Math.max(0, Math.min(ordered.length - 1, toIndex));
  if (from === clamped) return ordered;
  const next = ordered.map(w => ({ ...w }));
  const [item] = next.splice(from, 1);
  next.splice(clamped, 0, item);
  return next.map((w, i) => ({ ...w, phoneOrder: i }));
}

function movePhoneWidget(widgets, id, delta) {
  const ordered = assignPhoneOrder(widgets);
  const idx = ordered.findIndex(w => w.id === id);
  if (idx < 0) return widgets;
  return reorderPhoneWidgets(ordered, id, idx + delta);
}

function buildDefaultDashboardLayout(ctx) {
  const widgets = [];
  if (ctx.hasMediacao) {
    widgets.push(
      { id: 'kpi-pipeline', x: 0, y: 0, w: 3, h: 4 },
      { id: 'kpi-valor', x: 3, y: 0, w: 3, h: 4 },
      { id: 'kpi-ganhos', x: 6, y: 0, w: 3, h: 4 },
      { id: 'kpi-pipeline-alto', x: 9, y: 0, w: 3, h: 4 },
      { id: 'kpi-comissao-forecast', x: 0, y: 4, w: 5, h: 6 },
      { id: 'kpi-mix-natureza', x: 5, y: 4, w: 4, h: 6 },
      { id: 'kpi-comissao', x: 9, y: 4, w: 3, h: 6 },
      { id: 'pipeline', x: 0, y: 10, w: 8, h: 12 },
      { id: 'activities-today', x: 8, y: 10, w: 4, h: 6 },
      { id: 'activities-recent', x: 8, y: 16, w: 4, h: 6 },
    );
    let sideY = 22;
    if (ctx.hasAngariacao) {
      widgets.push({ id: 'kpi-angariacao', x: 0, y: sideY, w: 4, h: 6 });
    }
    if (ctx.hasIntermediacao) {
      widgets.push({ id: 'kpi-intermediacao', x: 4, y: sideY, w: 4, h: 6 });
    }
    if (ctx.hasCommission) {
      widgets.push({ id: 'comissao', x: 8, y: sideY, w: 4, h: 6 });
    }
    sideY += 6;
    // Bottom row: Ranking | Por operação | Autoavaliação (same y, equal height)
    widgets.push(
      { id: 'leaderboard', x: 0, y: sideY, w: 4, h: 8 },
      { id: 'operacao-chart', x: 4, y: sideY, w: 5, h: 8 },
      { id: 'avaliacao-trimestral', x: 9, y: sideY, w: 3, h: 8, phoneOrder: 99 },
    );
    if (ctx.hasConsultor) {
      widgets.push({ id: 'challenges', x: 0, y: sideY + 8, w: 4, h: 4 });
    }
  } else {
    widgets.push({ id: 'avaliacao-trimestral', x: 9, y: 0, w: 3, h: 8, phoneOrder: 99 });
  }
  if (ctx.hasSoi) {
    const maxY = widgets.reduce((m, w) => Math.max(m, (w.y || 0) + (w.h || 4)), 0);
    widgets.push({ id: 'soi', x: 0, y: maxY, w: 4, h: 6 });
  }
  return ensureAvaliacaoWidget(widgets.map(clampDashboardWidget));
}

/** Autoavaliação is a floating action, not a dashboard card. The neighbour on the right grows into the freed columns. */
function ensureAvaliacaoWidget(widgets) {
  const list = (widgets || []).map(w => ({ ...w }));
  const card = list.find(w => w.id === 'avaliacao-trimestral');
  const rest = list.filter(w => w.id !== 'avaliacao-trimestral');
  if (!card) return rest.map(clampDashboardWidget);
  const rightEdge = (card.x || 0) + (card.w || 3);
  const neighbour = rest.find(w => w.y === card.y && w.x === rightEdge);
  return rest.map(w => {
    if (neighbour && w.id === neighbour.id) {
      return clampDashboardWidget({ ...w, x: card.x || 0, w: (w.w || 3) + (card.w || 3) });
    }
    return clampDashboardWidget(w);
  });
}

function pinAvaliacaoWidgetFirst(widgets) {
  // Legacy name — keep widget present without forcing top-left.
  return ensureAvaliacaoWidget(widgets);
}

function migrateDashboardLayoutUnits(saved) {
  const widgets = (saved?.widgets || []).map(w => ({ ...w }));
  const v = Number(saved?.v) || 0;
  const alreadyFine = v >= DASHBOARD_LAYOUT_VERSION
    || widgets.some(w => (Number(w.h) || 0) >= 8);
  if (!widgets.length || alreadyFine) {
    return { widgets, v: DASHBOARD_LAYOUT_VERSION };
  }
  return {
    v: DASHBOARD_LAYOUT_VERSION,
    widgets: widgets.map(w => {
      const next = {
        ...w,
        y: (Number(w.y) || 0) * 2,
        h: Math.max(1, (Number(w.h) || 2) * 2),
      };
      const phoneH = w.phoneH ?? w.PhoneH;
      if (phoneH != null) next.phoneH = Math.max(1, Number(phoneH) * 2);
      return next;
    }),
  };
}

function normalizeDashboardLayout(saved, ctx) {
  const available = new Set(getAvailableDashboardWidgetIds(ctx));
  const migrated = migrateDashboardLayoutUnits(saved);
  let widgets = (migrated.widgets || [])
    .map(w => {
      // Rename legacy "progress" ring widget → Comissão card.
      if (w.id === 'progress') return { ...w, id: 'comissao' };
      return w;
    })
    .filter(w => {
      const id = w.id === 'progress' ? 'comissao' : w.id;
      return Boolean(DASHBOARD_WIDGET_DEFS[id]);
    })
    .map(w => {
      // Compact default for Avaliação card (was 4×3 in v3 units).
      if (w.id === 'avaliacao-trimestral' && w.w === 4 && (w.h === 3 || w.h === 6)) {
        return clampDashboardWidget({ ...w, w: 3, h: 4 });
      }
      if (w.id === 'comissao' && (w.h || 0) < 6) {
        return clampDashboardWidget({ ...w, h: 6 });
      }
      return clampDashboardWidget(w);
    });
  if (!widgets.length) return { widgets: buildDefaultDashboardLayout(ctx), v: DASHBOARD_LAYOUT_VERSION };
  if (available.has('comissao') && !widgets.some(w => w.id === 'comissao' || w.id === 'progress')) {
    const def = DASHBOARD_WIDGET_DEFS.comissao;
    const maxY = widgets.reduce((m, w) => Math.max(m, w.y + w.h), 0);
    widgets.push(clampDashboardWidget({
      id: 'comissao',
      x: 8,
      y: maxY,
      w: def.defaultW,
      h: def.defaultH,
    }));
  }
  if (available.has('operacao-chart') && !widgets.some(w => w.id === 'operacao-chart')) {
    const def = DASHBOARD_WIDGET_DEFS['operacao-chart'];
    const maxY = widgets.reduce((m, w) => Math.max(m, w.y + w.h), 0);
    widgets.push(clampDashboardWidget({
      id: 'operacao-chart',
      x: 8,
      y: maxY,
      w: def.defaultW,
      h: def.defaultH,
    }));
  }
  if (available.has('soi') && !widgets.some(w => w.id === 'soi')) {
    const def = DASHBOARD_WIDGET_DEFS.soi;
    const maxY = widgets.reduce((m, w) => Math.max(m, w.y + w.h), 0);
    widgets.push(clampDashboardWidget({
      id: 'soi',
      x: 0,
      y: maxY,
      w: def.defaultW,
      h: def.defaultH,
    }));
  }
  return { widgets: pinAvaliacaoWidgetFirst(widgets), v: DASHBOARD_LAYOUT_VERSION };
}

function dashboardWidgetOverlaps(a, b) {
  return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
}

function sortDashboardWidgets(widgets) {
  return [...widgets].sort((a, b) => (a.y - b.y) || (a.x - b.x) || String(a.id).localeCompare(String(b.id)));
}

/** Push overlapping widgets downward until nothing collides (active item stays put). */
function resolveDashboardCollisions(widgets, activeId) {
  const next = widgets.map(w => ({ ...w }));
  const active = next.find(w => w.id === activeId);
  if (!active) return next;

  let safety = 0;
  let changed = true;
  while (changed && safety < 120) {
    changed = false;
    safety += 1;
    // Always keep active fixed; push any overlapping item below it (or below the blocker).
    for (const item of next) {
      if (item.id === activeId) continue;
      if (dashboardWidgetOverlaps(item, active)) {
        const newY = active.y + active.h;
        if (item.y !== newY) {
          item.y = newY;
          changed = true;
        }
      }
    }
    // Cascade: resolve overlaps among non-active items (push lower ones further down).
    const ordered = sortDashboardWidgets(next.filter(w => w.id !== activeId));
    for (let i = 0; i < ordered.length; i += 1) {
      for (let j = i + 1; j < ordered.length; j += 1) {
        const a = ordered[i];
        const b = ordered[j];
        if (!dashboardWidgetOverlaps(a, b)) continue;
        const newY = a.y + a.h;
        if (b.y !== newY) {
          b.y = newY;
          changed = true;
        }
      }
    }
  }
  return next.map(clampDashboardWidget);
}

/** Pack widgets upward into free space (vertical compact). */
function compactDashboardLayout(widgets, { skipId = null } = {}) {
  const all = widgets.map(w => ({ ...w }));
  const fixed = skipId ? all.find(w => w.id === skipId) : null;
  const others = sortDashboardWidgets(all.filter(w => w.id !== skipId));
  const placed = [];
  if (fixed) placed.push({ ...fixed });

  for (const item of others) {
    let y = 0;
    let bestY = item.y;
    while (y <= item.y) {
      const candidate = { ...item, y };
      if (!placed.some(p => dashboardWidgetOverlaps(candidate, p))) {
        bestY = y;
        break;
      }
      y += 1;
    }
    item.y = bestY;
    let safety = 0;
    while (placed.some(p => dashboardWidgetOverlaps(item, p)) && safety < 80) {
      item.y += 1;
      safety += 1;
    }
    placed.push(item);
  }
  return placed.map(clampDashboardWidget);
}

function placeDashboardWidget(widgets, movingId, patch) {
  const base = widgets.map(w => (w.id === movingId ? clampDashboardWidget({ ...w, ...patch }) : { ...w }));
  const resolved = resolveDashboardCollisions(base, movingId);
  return compactDashboardLayout(resolved, { skipId: movingId });
}

function findFreeDashboardSpot(widgets, w, h) {
  for (let y = 0; y < 80; y += 1) {
    for (let x = 0; x <= DASHBOARD_GRID_COLS - w; x += 1) {
      const candidate = { x, y, w, h };
      if (!widgets.some(existing => dashboardWidgetOverlaps(candidate, existing))) {
        return { x, y };
      }
    }
  }
  const maxY = widgets.reduce((m, item) => Math.max(m, item.y + item.h), 0);
  return { x: 0, y: maxY };
}

const PIPELINE_CARD_ESTADOS = ['Em Curso', 'Concluído', 'Perdido'];
const PIPELINE_CARD_NATUREZAS = ['Venda', 'Arrendamento'];
const PIPELINE_CARD_PROBS = ['Alta', 'Média', 'Baixa'];
const PIPELINE_CARD_DEFAULT_FILTERS = {
  estados: [...PIPELINE_CARD_ESTADOS],
  naturezas: [...PIPELINE_CARD_NATUREZAS],
  probabilidades: [],
};

function PipelineCard({ ano = '', mes = '', onInspect = null, refreshKey = 0, onExplore = null }) {
  const [columns, setColumns] = useState([]);
  const [filterHint, setFilterHint] = useState('');
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [showFilters, setShowFilters] = useState(false);
  const [estados, setEstados] = useState(() => [...PIPELINE_CARD_DEFAULT_FILTERS.estados]);
  const [naturezas, setNaturezas] = useState(() => [...PIPELINE_CARD_DEFAULT_FILTERS.naturezas]);
  const [probabilidades, setProbabilidades] = useState(() => [...PIPELINE_CARD_DEFAULT_FILTERS.probabilidades]);
  const [applied, setApplied] = useState(() => ({
    estados: [...PIPELINE_CARD_DEFAULT_FILTERS.estados],
    naturezas: [...PIPELINE_CARD_DEFAULT_FILTERS.naturezas],
    probabilidades: [...PIPELINE_CARD_DEFAULT_FILTERS.probabilidades],
  }));
  const filterBtnRef = useRef(null);
  const popoverRef = useRef(null);
  const [popoverPos, setPopoverPos] = useState(null);

  const load = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const params = new URLSearchParams();
      if (ano) params.set('ano', String(ano));
      if (mes) params.set('mes', String(mes));
      params.set('estados', applied.estados.join(','));
      params.set('naturezas', applied.naturezas.join(','));
      params.set('probabilidades', applied.probabilidades.join(','));
      const res = await apiFetch(`/api/dashboard/pipeline?${params}`);
      setColumns(res.columns || res.Columns || []);
      setFilterHint(res.filterHint || res.FilterHint || '');
    } catch (err) {
      setError(err.message || 'Não foi possível carregar o pipeline.');
      setColumns([]);
      setFilterHint('');
    } finally {
      setLoading(false);
    }
  }, [ano, mes, applied, refreshKey]);

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

  const toggleIn = (list, value, setter) => {
    setter(list.includes(value) ? list.filter(v => v !== value) : [...list, value]);
  };

  const applyFilters = () => {
    setApplied({
      estados: [...estados],
      naturezas: [...naturezas],
      probabilidades: [...probabilidades],
    });
    setShowFilters(false);
  };

  const clearFilters = () => {
    setEstados([...PIPELINE_CARD_DEFAULT_FILTERS.estados]);
    setNaturezas([...PIPELINE_CARD_DEFAULT_FILTERS.naturezas]);
    setProbabilidades([...PIPELINE_CARD_DEFAULT_FILTERS.probabilidades]);
    setApplied({
      estados: [...PIPELINE_CARD_DEFAULT_FILTERS.estados],
      naturezas: [...PIPELINE_CARD_DEFAULT_FILTERS.naturezas],
      probabilidades: [...PIPELINE_CARD_DEFAULT_FILTERS.probabilidades],
    });
    setShowFilters(false);
  };

  const openPopover = useCallback(() => {
    const btn = filterBtnRef.current;
    if (!btn) return;
    const r = btn.getBoundingClientRect();
    const width = 360;
    const margin = 12;
    const left = Math.max(margin, Math.min(r.left, window.innerWidth - width - margin));
    const top = Math.min(r.bottom + 8, window.innerHeight - 420);
    setPopoverPos({ left, top });
    setShowFilters(true);
  }, []);

  const closePopover = useCallback(() => setShowFilters(false), []);

  useEffect(() => {
    if (!showFilters) return undefined;
    const onDoc = (e) => {
      const t = e.target;
      if (popoverRef.current?.contains(t)) return;
      if (filterBtnRef.current?.contains(t)) return;
      closePopover();
    };
    const onKey = (e) => { if (e.key === 'Escape') closePopover(); };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onKey);
    const onScroll = () => openPopover();
    window.addEventListener('scroll', onScroll, true);
    window.addEventListener('resize', onScroll);
    return () => {
      document.removeEventListener('mousedown', onDoc);
      document.removeEventListener('keydown', onKey);
      window.removeEventListener('scroll', onScroll, true);
      window.removeEventListener('resize', onScroll);
    };
  }, [showFilters, closePopover, openPopover]);

  const filterCount = (applied.estados.length !== PIPELINE_CARD_ESTADOS.length ? 1 : 0)
    + (applied.naturezas.length !== PIPELINE_CARD_NATUREZAS.length ? 1 : 0)
    + (applied.probabilidades.length > 0 ? 1 : 0);

  const filterSummaryParts = [];
  if (applied.estados.length === 0) {
    filterSummaryParts.push('Sem estados');
  } else if (applied.estados.length < PIPELINE_CARD_ESTADOS.length) {
    filterSummaryParts.push(applied.estados.join(' · '));
  } else {
    filterSummaryParts.push('Todos os estados');
  }
  if (applied.naturezas.length === 0) {
    filterSummaryParts.push('Sem natureza');
  } else if (applied.naturezas.length < PIPELINE_CARD_NATUREZAS.length) {
    filterSummaryParts.push(applied.naturezas.join(' · '));
  }
  if (applied.probabilidades.length > 0) {
    filterSummaryParts.push(`Prob. ${applied.probabilidades.join(' · ')}`);
  }
  const filterSummary = filterSummaryParts.join(' · ');
  const filterSummaryTitle = [filterHint, filterSummary].filter(Boolean).join(' — ');

  return (
    <DashCard
      className={`pipeline-widget-card${showFilters ? ' filters-open' : ''}`}
      icon="kanban"
      onExplore={onExplore}
      period={dashboardPeriodLabel(ano, mes)}
      title={(
        <>
          Pipeline
          <DashboardFilterStar hint={filterSummaryTitle || filterHint} />
          {filterSummary ? (
            <AppTooltip label={filterSummaryTitle || filterSummary}>
              <span className="pipeline-filter-summary">{filterSummary}</span>
            </AppTooltip>
          ) : null}
        </>
      )}
      actions={(
        <div className="ds-card-head-actions">
          {typeof onInspect === 'function' && (
            <button
              type="button"
              className="dashboard-inspect-btn"
              title="Inspeção admin (filtro partilhado)"
              aria-label="Inspeção admin"
              onClick={onInspect}
            >
              <Icon name="sliders" size={14} />
            </button>
          )}
          <button
            type="button"
            className={`btn-filter-toggle btn-filter-chart${showFilters ? ' active' : ''}`}
            aria-pressed={showFilters}
            aria-label="Filtros do pipeline"
            title="Filtros"
            ref={filterBtnRef}
            onClick={() => {
              if (showFilters) closePopover();
              else openPopover();
            }}
          >
            <Icon name="filter" size={14} />
            {filterCount > 0 ? <span className="operacao-chart-filter-badge">{filterCount}</span> : null}
          </button>
        </div>
      )}
    >
      {error ? (
        <p className="empty-hint">{error}</p>
      ) : (
        <PipelineKanban
          columns={columns}
          loading={loading}
          visibleEstados={applied.estados.length > 0 ? applied.estados : null}
        />
      )}
      {showFilters && popoverPos && ReactDOM.createPortal(
        <div
          className="chart-filter-popover"
          role="dialog"
          aria-label="Filtros do pipeline"
          ref={popoverRef}
          style={{ left: popoverPos.left, top: popoverPos.top }}
        >
          <div className="chart-filter-popover-header">
            <strong className="chart-filter-popover-title">Filtros do Pipeline</strong>
          </div>
          <div className="chart-filter-popover-body">
            <div className="checkbox-group">
              <span className="checkbox-group-title">Estado</span>
              <div className="checkbox-group-chips">
                {PIPELINE_CARD_ESTADOS.map(opt => (
                  <label key={opt} className="avaliacao-check">
                    <input
                      type="checkbox"
                      checked={estados.includes(opt)}
                      onChange={() => toggleIn(estados, opt, setEstados)}
                    />
                    <span>{opt}</span>
                  </label>
                ))}
              </div>
            </div>
            <div className="checkbox-group">
              <span className="checkbox-group-title">Natureza</span>
              <div className="checkbox-group-chips">
                {PIPELINE_CARD_NATUREZAS.map(opt => (
                  <label key={opt} className="avaliacao-check">
                    <input
                      type="checkbox"
                      checked={naturezas.includes(opt)}
                      onChange={() => toggleIn(naturezas, opt, setNaturezas)}
                    />
                    <span>{opt}</span>
                  </label>
                ))}
              </div>
            </div>
            <ProbabilidadeMultiSelect variant="chips" value={probabilidades} onChange={setProbabilidades} />
            <p className="dashboard-inspect-filter-hint" style={{ marginTop: '0.5rem' }}>
              Ano vem do filtro do dashboard. Use <strong>Estado</strong> / Natureza / Probabilidade para afinar os totais.
            </p>
          </div>
          <div className="chart-filter-popover-actions">
            <button type="button" className="btn btn-secondary btn-sm" onClick={clearFilters}>Repor</button>
            <button type="button" className="btn btn-primary btn-sm" onClick={applyFilters}>Aplicar</button>
          </div>
        </div>,
        document.body
      )}
    </DashCard>
  );
}

function DashboardInspectModal({ widgetId, widgetLabel, ano, mes, onClose, onFilterSaved = null }) {
  const PAGE_SIZE = 10;
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [data, setData] = useState(null);
  const [editingFilter, setEditingFilter] = useState(false);
  const [filterMode, setFilterMode] = useState('open');
  const [filterEstados, setFilterEstados] = useState([]);
  const [filterNaturezas, setFilterNaturezas] = useState([]);
  const [filterProbabilidades, setFilterProbabilidades] = useState([]);
  const [filterValidados, setFilterValidados] = useState([]);
  const [filterAggregation, setFilterAggregation] = useState('count');
  const [savingFilter, setSavingFilter] = useState(false);
  const [filterMessage, setFilterMessage] = useState(null);
  const [reloadKey, setReloadKey] = useState(0);
  const [pageIndex, setPageIndex] = useState(0);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      setLoading(true);
      setError(null);
      setFilterMessage(null);
      setPageIndex(0);
      try {
        const qs = new URLSearchParams({ widget: widgetId });
        if (ano) qs.set('ano', String(ano));
        if (mes) qs.set('mes', String(mes));
        const res = await apiFetch(`/api/dashboard/inspect?${qs}`);
        if (!cancelled) {
          setData(res);
          const f = res?.filters || res?.Filters || {};
          const cfg = f.filterConfig || f.FilterConfig || {};
          setFilterMode(String(cfg.mode || cfg.Mode || 'open').toLowerCase());
          setFilterEstados(cfg.estados || cfg.Estados || []);
          setFilterNaturezas(cfg.naturezas || cfg.Naturezas || []);
          setFilterProbabilidades(cfg.probabilidades || cfg.Probabilidades || []);
          setFilterValidados(cfg.validados || cfg.Validados || []);
          setFilterAggregation(cfg.aggregation || cfg.Aggregation || 'count');
          setEditingFilter(false);
        }
      } catch (err) {
        if (!cancelled) setError(err.message || 'Não foi possível carregar a inspeção.');
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, [widgetId, ano, mes, reloadKey]);

  const filters = data?.filters || data?.Filters || {};
  const counts = data?.counts || data?.Counts || {};
  const dist = data?.approvalDistribution || data?.ApprovalDistribution || [];
  const items = data?.items || data?.Items || [];
  const filterEditable = !!(filters.filterEditable ?? filters.FilterEditable);
  const filterModes = filters.filterModes || filters.FilterModes || [
    { value: 'open', label: 'Estado aberto (não Concluído / Perdido)' },
    { value: 'won', label: 'Estado Concluído / Ganho' },
    { value: 'lost', label: 'Estado Perdido' },
    { value: 'all', label: 'Todos os estados' },
    { value: 'estados', label: 'Estados específicos' },
    { value: 'default', label: 'Repor predefinição' },
  ];
  const estadoStages = filters.estadoStages || filters.EstadoStages || ['Em Curso', 'Concluído', 'Perdido'];
  const validadoOptions = filters.validadoOptions || filters.ValidadoOptions || ['Aprovado', 'Pendente', 'Reprovado'];
  const naturezaOptions = filters.naturezaOptions || filters.NaturezaOptions || ['Venda', 'Arrendamento'];
  const probabilidadeOptions = filters.probabilidadeOptions || filters.ProbabilidadeOptions || ['Alta', 'Média', 'Baixa'];
  const isKpiWidget = String(widgetId || '').startsWith('kpi-');
  const isPipelineWidget = String(widgetId || '').toLowerCase() === 'pipeline';
  const widgetFilterLabel = filters.widgetFilter || filters.WidgetFilter || '—';
  const pageCount = Math.max(1, Math.ceil(items.length / PAGE_SIZE));
  const safePage = Math.min(pageIndex, pageCount - 1);
  const pageItems = items.slice(safePage * PAGE_SIZE, safePage * PAGE_SIZE + PAGE_SIZE);

  const toggleInList = (setter) => (value) => {
    setter(prev => {
      const has = prev.some(e => String(e).toLowerCase() === String(value).toLowerCase());
      if (has) return prev.filter(e => String(e).toLowerCase() !== String(value).toLowerCase());
      return [...prev, value];
    });
  };
  const toggleEstado = toggleInList(setFilterEstados);
  const toggleNatureza = toggleInList(setFilterNaturezas);
  const toggleProbabilidade = toggleInList(setFilterProbabilidades);
  const toggleValidado = toggleInList(setFilterValidados);

  const saveFilter = async () => {
    setSavingFilter(true);
    setFilterMessage(null);
    try {
      await apiFetch(`/api/dashboard/widget-filters/${encodeURIComponent(widgetId)}`, {
        method: 'PUT',
        body: JSON.stringify({
          mode: filterMode,
          estados: filterMode === 'estados' ? filterEstados : [],
          naturezas: filterNaturezas,
          probabilidades: filterProbabilidades,
          validados: filterValidados,
          aggregation: isKpiWidget ? filterAggregation : undefined,
        }),
      });
      setFilterMessage('Filtro guardado. Todos os utilizadores passam a ver o cartão com este filtro.');
      setReloadKey(k => k + 1);
      if (typeof onFilterSaved === 'function') onFilterSaved(widgetId);
    } catch (err) {
      setFilterMessage(err.message || 'Não foi possível guardar o filtro.');
    } finally {
      setSavingFilter(false);
    }
  };

  const estadoTone = (value) => {
    const v = String(value || '').toLowerCase();
    if (v.includes('conclu') || v.includes('ganho')) return 'won';
    if (v.includes('perdido')) return 'lost';
    if (v.includes('curso')) return 'open';
    return 'neutral';
  };

  const dismiss = useOverlayDismiss(onClose);

  return ReactDOM.createPortal(
    <div className="negocio-modal-overlay" role="presentation" {...dismiss}>
      <div
        className="negocio-modal dashboard-inspect-modal"
        role="dialog"
        aria-modal="true"
        aria-label={`Inspeção ${widgetLabel || widgetId}`}
        onClick={e => e.stopPropagation()}
      >
        <div className="negocio-modal-header dashboard-inspect-header">
          <div className="dashboard-inspect-header-copy">
            <span className="dashboard-inspect-kicker">Inspeção admin</span>
            <h3>{widgetLabel || widgetId}</h3>
            <p>Filtros partilhados, contagens e amostra dos dados usados neste cartão.</p>
          </div>
          <button type="button" className="btn-icon-ghost dashboard-inspect-close" onClick={onClose} aria-label="Fechar">
            <Icon name="close" size={16} />
          </button>
        </div>
        <div className="negocio-modal-body dashboard-inspect-body">
          {loading && <LoadingSpinner message="A carregar filtros e dados..." />}
          {error && <ErrorBanner error={{ message: error }} onDismiss={() => setError(null)} />}
          {!loading && !error && data && (
            <>
              <section className="dashboard-inspect-section dashboard-inspect-hero">
                <div className="dashboard-inspect-filter-card">
                  <div className="dashboard-inspect-filter-card-top">
                    <div>
                      <span className="dashboard-inspect-kicker">Filtro do cartão</span>
                      {!editingFilter ? (
                        <p className="dashboard-inspect-filter-title">{widgetFilterLabel}</p>
                      ) : (
                        <p className="dashboard-inspect-filter-title">Editar filtro partilhado</p>
                      )}
                    </div>
                    {!editingFilter && filterEditable && (
                      <button type="button" className="btn btn-secondary btn-sm" onClick={() => setEditingFilter(true)}>
                        <Icon name="edit" size={14} />
                        Editar
                      </button>
                    )}
                  </div>

                  {!editingFilter ? (
                    <div className="dashboard-inspect-meta-grid">
                      <div className="dashboard-inspect-meta-chip">
                        <span>Campo estado</span>
                        <strong>{filters.estadoField || filters.EstadoField || 'Estado'}</strong>
                      </div>
                      <div className="dashboard-inspect-meta-chip">
                        <span>Ano</span>
                        <strong>{filters.ano ?? filters.Ano ?? 'Todos'}</strong>
                      </div>
                      <div className="dashboard-inspect-meta-chip">
                        <span>Scope all</span>
                        <strong>{String(filters.scopeAll ?? filters.ScopeAll ?? '—')}</strong>
                      </div>
                      <div className="dashboard-inspect-meta-chip dashboard-inspect-meta-chip-wide">
                        <span>Scope email</span>
                        <strong>{filters.scopeEmail || filters.ScopeEmail || '—'}</strong>
                      </div>
                      <div className="dashboard-inspect-meta-chip dashboard-inspect-meta-chip-wide">
                        <span>Estados pipeline</span>
                        <strong>{(estadoStages || []).join(' · ') || 'Em Curso · Concluído · Perdido'}</strong>
                      </div>
                    </div>
                  ) : (
                    <div className="dashboard-inspect-filter-editor">
                      <label>
                        <span>Modo</span>
                        <AppSelect
                          value={filterMode}
                          options={filterModes.map(m => ({ value: m.value || m.Value, label: m.label || m.Label }))}
                          onChange={setFilterMode}
                          ariaLabel="Modo do filtro do cartão"
                        />
                      </label>
                      {filterMode === 'estados' && (
                        <div className="dashboard-inspect-estado-checks" role="group" aria-label="Estados">
                          {estadoStages.map(estado => (
                            <label key={estado} className="dashboard-inspect-check">
                              <input
                                type="checkbox"
                                checked={filterEstados.some(e => String(e).toLowerCase() === String(estado).toLowerCase())}
                                onChange={() => toggleEstado(estado)}
                              />
                              <span>{estado}</span>
                            </label>
                          ))}
                        </div>
                      )}
                      {(isPipelineWidget || isKpiWidget) && filterMode !== 'default' && (
                        <>
                          {!isPipelineWidget && (
                            <div className="dashboard-inspect-estado-checks" role="group" aria-label="Validado">
                              <span className="checkbox-group-title">Validado</span>
                              {validadoOptions.map(opt => (
                                <label key={opt} className="dashboard-inspect-check">
                                  <input
                                    type="checkbox"
                                    checked={filterValidados.some(e => String(e).toLowerCase() === String(opt).toLowerCase())}
                                    onChange={() => toggleValidado(opt)}
                                  />
                                  <span>{opt}</span>
                                </label>
                              ))}
                            </div>
                          )}
                          <div className="dashboard-inspect-estado-checks" role="group" aria-label="Natureza">
                            <span className="checkbox-group-title">Natureza</span>
                            {naturezaOptions.map(opt => (
                              <label key={opt} className="dashboard-inspect-check">
                                <input
                                  type="checkbox"
                                  checked={filterNaturezas.some(e => String(e).toLowerCase() === String(opt).toLowerCase())}
                                  onChange={() => toggleNatureza(opt)}
                                />
                                <span>{opt}</span>
                              </label>
                            ))}
                          </div>
                          <ProbabilidadeMultiSelect
                            value={filterProbabilidades}
                            options={probabilidadeOptions}
                            onChange={setFilterProbabilidades}
                          />
                        </>
                      )}
                      {isKpiWidget && filterMode !== 'default' && (
                        <label>
                          <span>Agregação</span>
                          <AppSelect
                            value={filterAggregation}
                            options={[
                              { value: 'count', label: 'Contagem' },
                              { value: 'sumMontante', label: 'Soma Montante' },
                              { value: 'sumComissao', label: 'Soma Comissão HABITA' },
                            ]}
                            onChange={setFilterAggregation}
                            ariaLabel="Agregação do KPI"
                          />
                        </label>
                      )}
                      <div className="dashboard-inspect-filter-actions">
                        <button
                          type="button"
                          className="btn btn-primary btn-sm"
                          disabled={savingFilter || (filterMode === 'estados' && filterEstados.length === 0)}
                          onClick={saveFilter}
                        >
                          {savingFilter ? 'A guardar…' : 'Guardar para todos'}
                        </button>
                        <button type="button" className="btn btn-secondary btn-sm" disabled={savingFilter} onClick={() => setEditingFilter(false)}>
                          Cancelar
                        </button>
                      </div>
                      <p className="dashboard-inspect-filter-hint">
                        Este filtro fica gravado no servidor e aplica-se a todos os utilizadores neste cartão.
                      </p>
                    </div>
                  )}
                  {filterMessage && <p className="dashboard-inspect-filter-msg">{filterMessage}</p>}
                  {(filters.note || filters.Note) && (
                    <p className="dashboard-inspect-note">{filters.note || filters.Note}</p>
                  )}
                </div>
              </section>

              <section className="dashboard-inspect-section">
                <div className="dashboard-inspect-section-head">
                  <h4>Contagens</h4>
                </div>
                <div className="dashboard-inspect-counts">
                  <div className="dashboard-inspect-stat">
                    <span>Lidos</span>
                    <strong>{counts.fetched ?? counts.Fetched ?? 0}</strong>
                  </div>
                  <div className="dashboard-inspect-stat">
                    <span>Após permissões</span>
                    <strong>{counts.afterScope ?? counts.AfterScope ?? 0}</strong>
                  </div>
                  <div className="dashboard-inspect-stat dashboard-inspect-stat-accent">
                    <span>Usados no cartão</span>
                    <strong>{counts.usedInWidget ?? counts.UsedInWidget ?? 0}</strong>
                  </div>
                </div>
              </section>

              <section className="dashboard-inspect-section">
                <div className="dashboard-inspect-section-head">
                  <h4>Distribuição Estado</h4>
                  <span className="dashboard-inspect-section-sub">Após permissões</span>
                </div>
                {dist.length === 0 ? (
                  <p className="empty-hint">Sem valores.</p>
                ) : (
                  <ul className="dashboard-inspect-dist">
                    {dist.map((row) => (
                      <li key={row.value || row.Value} className={`tone-${estadoTone(row.value || row.Value)}`}>
                        <strong>{row.value || row.Value}</strong>
                        <span>{row.count ?? row.Count}</span>
                      </li>
                    ))}
                  </ul>
                )}
              </section>

              <section className="dashboard-inspect-section dashboard-inspect-data-section">
                <div className="dashboard-inspect-section-head">
                  <h4>Dados usados no cálculo</h4>
                  <span className="dashboard-inspect-section-sub">{items.length} linhas</span>
                </div>
                <div className="dashboard-inspect-table-shell">
                  <div className="data-table-wrap dashboard-inspect-table-wrap">
                    <table className="data-table">
                      <thead>
                        <tr>
                          <th>Referência</th>
                          <th>Consultor</th>
                          <th>Estado</th>
                          <th>Operação</th>
                          <th>Montante</th>
                          <th>Comissão</th>
                        </tr>
                      </thead>
                      <tbody>
                        {pageItems.length === 0 ? (
                          <tr><td colSpan={6} className="dashboard-inspect-empty-cell">Sem linhas após filtros.</td></tr>
                        ) : pageItems.map((row) => (
                          <tr key={row.id || row.Id}>
                            <td>{row.referencia || row.Referencia || '—'}</td>
                            <td>{row.consultor || row.Consultor || '—'}</td>
                            <td>
                              <span className={`dashboard-inspect-estado-pill tone-${estadoTone(row.estado || row.Estado)}`}>
                                {row.estado || row.Estado || '—'}
                              </span>
                            </td>
                            <td>{row.operacao || row.Operacao || '—'}</td>
                            <td className="num">{formatCurrency(row.montante ?? row.Montante)}</td>
                            <td className="num">{formatCurrency(row.comissao ?? row.Comissao)}</td>
                          </tr>
                        ))}
                      </tbody>
                    </table>
                  </div>
                  <div className="pagination dashboard-inspect-pagination">
                    <span>
                      Linhas: {items.length}
                      {' · '}
                      Página {safePage + 1} / {pageCount}
                    </span>
                    <span>
                      <button
                        type="button"
                        className="btn btn-secondary btn-icon"
                        onClick={() => setPageIndex(p => Math.max(0, p - 1))}
                        disabled={safePage <= 0}
                        title="Anterior"
                      >
                        <Icon name="arrowLeft" size={16} />
                      </button>
                      {' '}
                      <button
                        type="button"
                        className="btn btn-secondary btn-icon"
                        onClick={() => setPageIndex(p => Math.min(pageCount - 1, p + 1))}
                        disabled={safePage >= pageCount - 1}
                        title="Seguinte"
                      >
                        <Icon name="arrowRight" size={16} />
                      </button>
                    </span>
                  </div>
                </div>
              </section>
            </>
          )}
        </div>
      </div>
    </div>,
    document.body
  );
}

function DashboardWidgetGrid({
  widgets,
  editMode,
  onChange,
  renderWidget,
  rowHeight = DASHBOARD_ROW_HEIGHT,
  inspectEnabled = false,
  onInspectWidget = null,
  canEditPinnedWidgets = false,
}) {
  const { isPhone } = useViewportNav();
  const layoutEdit = Boolean(editMode);
  const gridRef = useRef(null);
  const dragRef = useRef(null);
  const resizeRef = useRef(null);
  const widgetsRef = useRef(widgets);
  const liveRef = useRef(null);
  const lastCellRef = useRef(null);
  const [liveWidgets, setLiveWidgets] = useState(null);
  const [draggingId, setDraggingId] = useState(null);
  const displayWidgets = liveWidgets || widgets;
  widgetsRef.current = displayWidgets;
  const stackedWidgets = useMemo(() => {
    if (!isPhone) return displayWidgets;
    return sortWidgetsForPhone(displayWidgets);
  }, [displayWidgets, isPhone]);

  const getGridMetrics = useCallback(() => {
    const el = gridRef.current;
    if (!el) {
      return { colStep: 80, rowStep: rowHeight, padLeft: 0, padTop: 0, left: 0, top: 0 };
    }
    const rect = el.getBoundingClientRect();
    const style = window.getComputedStyle(el);
    const colGap = parseFloat(style.columnGap || style.gap || '12') || 12;
    const rowGap = parseFloat(style.rowGap || style.gap || '12') || 12;
    const padLeft = parseFloat(style.paddingLeft || '0') || 0;
    const padTop = parseFloat(style.paddingTop || '0') || 0;
    const padRight = parseFloat(style.paddingRight || '0') || 0;
    const innerW = Math.max(1, rect.width - padLeft - padRight);
    const colWidth = (innerW - colGap * (DASHBOARD_GRID_COLS - 1)) / DASHBOARD_GRID_COLS;
    return {
      colStep: colWidth + colGap,
      rowStep: rowHeight + rowGap,
      padLeft,
      padTop,
      left: rect.left,
      top: rect.top,
    };
  }, [rowHeight]);

  const pointerToCell = useCallback((clientX, clientY) => {
    const m = getGridMetrics();
    const x = Math.floor((clientX - m.left - m.padLeft) / m.colStep);
    const y = Math.floor((clientY - m.top - m.padTop) / m.rowStep);
    return {
      x: Math.max(0, Math.min(DASHBOARD_GRID_COLS - 1, x)),
      y: Math.max(0, y),
    };
  }, [getGridMetrics]);

  const scrollEditSurface = useCallback((clientY) => {
    const scroller = gridRef.current?.closest('.dashboard-v2-scroll');
    if (!scroller) return;
    const r = scroller.getBoundingClientRect();
    const zone = 56;
    if (clientY < r.top + zone) scroller.scrollBy(0, -28);
    else if (clientY > r.bottom - zone) scroller.scrollBy(0, 28);
  }, []);

  const pointerToPhoneIndex = useCallback((clientY) => {
    const el = gridRef.current;
    if (!el) return 0;
    const cards = [...el.querySelectorAll(':scope > .dashboard-widget')];
    for (let i = 0; i < cards.length; i += 1) {
      const r = cards[i].getBoundingClientRect();
      if (clientY < r.top + r.height / 2) return i;
    }
    return Math.max(0, cards.length - 1);
  }, []);

  const applyWidgetChange = useCallback((next) => {
    liveRef.current = next;
    setLiveWidgets(next);
  }, []);

  const commitWidgets = useCallback((next) => {
    const prepared = isPhone
      ? assignPhoneOrder(next.map(clampDashboardWidget))
      : compactDashboardLayout(next.map(clampDashboardWidget));
    liveRef.current = null;
    lastCellRef.current = null;
    setLiveWidgets(null);
    setDraggingId(null);
    onChange(prepared);
  }, [onChange, isPhone]);

  useEffect(() => {
    if (!layoutEdit) return undefined;
    const onMove = (e) => {
      const source = widgetsRef.current;
      if (dragRef.current) {
        const { id, w, h, origin, grabX = 0, grabY = 0 } = dragRef.current;
        scrollEditSurface(e.clientY);
        if (isPhone) {
          const toIndex = pointerToPhoneIndex(e.clientY);
          const key = `phone:${id}:${toIndex}`;
          if (lastCellRef.current === key) return;
          lastCellRef.current = key;
          applyWidgetChange(reorderPhoneWidgets(origin || source, id, toIndex));
          return;
        }
        const cell = pointerToCell(e.clientX, e.clientY);
        const x = Math.max(0, Math.min(DASHBOARD_GRID_COLS - w, cell.x - grabX));
        const y = Math.max(0, cell.y - grabY);
        const key = `${id}:${x},${y}`;
        if (lastCellRef.current === key) return;
        lastCellRef.current = key;
        const base = (origin || source).map(item => ({ ...item }));
        applyWidgetChange(placeDashboardWidget(base, id, { x, y, w, h }));
      }
      if (resizeRef.current) {
        if (isPhone) {
          const { id, startY, phoneOriginH, origin } = resizeRef.current;
          const deltaRows = Math.round((e.clientY - startY) / rowHeight);
          const nextH = clampPhoneWidgetH(id, (phoneOriginH || 0) + deltaRows);
          const key = `phone-resize:${id}:${nextH}`;
          if (lastCellRef.current === key) return;
          lastCellRef.current = key;
          applyWidgetChange((origin || source).map(item => (
            item.id === id ? { ...item, phoneH: nextH } : { ...item }
          )));
          return;
        }
        scrollEditSurface(e.clientY);
        const { id, originX, originY, originW, originH, startX, startY, origin } = resizeRef.current;
        const def = DASHBOARD_WIDGET_DEFS[id] || { minW: 2, minH: 2 };
        const metrics = getGridMetrics();
        const dW = Math.round((e.clientX - startX) / metrics.colStep);
        const dH = Math.round((e.clientY - startY) / metrics.rowStep);
        const w = Math.max(def.minW, Math.min(DASHBOARD_GRID_COLS - originX, (originW || def.minW) + dW));
        const h = Math.max(def.minH, (originH || def.minH) + dH);
        const key = `resize:${id}:${w},${h}`;
        if (lastCellRef.current === key) return;
        lastCellRef.current = key;
        const base = (origin || source).map(item => ({ ...item }));
        applyWidgetChange(placeDashboardWidget(base, id, { x: originX, y: originY, w, h }));
      }
    };
    const onUp = () => {
      if (dragRef.current || resizeRef.current) {
        commitWidgets(liveRef.current || widgetsRef.current);
      }
      dragRef.current = null;
      resizeRef.current = null;
      lastCellRef.current = null;
      setDraggingId(null);
    };
    window.addEventListener('pointermove', onMove);
    window.addEventListener('pointerup', onUp);
    return () => {
      window.removeEventListener('pointermove', onMove);
      window.removeEventListener('pointerup', onUp);
    };
  }, [layoutEdit, isPhone, rowHeight, pointerToCell, pointerToPhoneIndex, applyWidgetChange, commitWidgets, scrollEditSurface, getGridMetrics]);

  const startDrag = (id, e) => {
    e.preventDefault();
    e.stopPropagation();
    const item = displayWidgets.find(x => x.id === id);
    if (!item) return;
    const def = DASHBOARD_WIDGET_DEFS[id];
    if (def?.pinned && !canEditPinnedWidgets) return; // Only non-admins are locked.
    const cell = pointerToCell(e.clientX, e.clientY);
    dragRef.current = {
      id,
      w: item.w,
      h: item.h,
      grabX: isPhone ? 0 : cell.x - item.x,
      grabY: isPhone ? 0 : cell.y - item.y,
      origin: displayWidgets.map(w => ({ ...w })),
    };
    lastCellRef.current = null;
    setDraggingId(id);
    try { e.currentTarget.setPointerCapture(e.pointerId); } catch { /* ignore */ }
  };

  const startResize = (id, e) => {
    e.preventDefault();
    e.stopPropagation();
    const item = displayWidgets.find(x => x.id === id);
    if (!item) return;
    const def = DASHBOARD_WIDGET_DEFS[id];
    if (def?.pinned && !canEditPinnedWidgets) return; // Only non-admins are locked.
    resizeRef.current = {
      id,
      originX: item.x,
      originY: item.y,
      originW: item.w,
      originH: item.h,
      startX: e.clientX,
      startY: e.clientY,
      phoneOriginH: resolvePhoneWidgetH(item),
      origin: displayWidgets.map(w => ({ ...w })),
    };
    lastCellRef.current = null;
    setDraggingId(id);
    try { e.currentTarget.setPointerCapture(e.pointerId); } catch { /* ignore */ }
  };

  const removeWidget = (id) => {
    if (DASHBOARD_WIDGET_DEFS[id]?.pinned) return;
    const next = displayWidgets.filter(w => w.id !== id);
    commitWidgets(isPhone ? next : compactDashboardLayout(next));
  };

  const shiftPhoneWidget = (id, delta) => {
    commitWidgets(movePhoneWidget(displayWidgets, id, delta));
  };

  const bumpPhoneWidgetH = (id, delta) => {
    const item = displayWidgets.find(w => w.id === id);
    if (!item) return;
    const nextH = clampPhoneWidgetH(id, resolvePhoneWidgetH(item) + delta);
    commitWidgets(displayWidgets.map(w => (w.id === id ? { ...w, phoneH: nextH } : w)));
  };

  const maxRow = displayWidgets.reduce((m, w) => Math.max(m, w.y + w.h), 0);
  const phoneCount = stackedWidgets.length;

  return (
    <div
      ref={gridRef}
      className={`dashboard-widget-grid${layoutEdit ? ' dashboard-widget-grid-editing' : ''}${isPhone ? ' dashboard-widget-grid-phone' : ''}`}
      style={isPhone ? undefined : {
        '--dash-row-height': `${rowHeight}px`,
        gridAutoRows: `${rowHeight}px`,
        minHeight: `${Math.max(maxRow + 1, 6) * rowHeight}px`,
      }}
    >
      {stackedWidgets.map(widget => {
        const def = DASHBOARD_WIDGET_DEFS[widget.id] || {};
        const isPinned = Boolean(def.pinned);
        const phoneRows = resolvePhoneWidgetH(widget);
        const phoneMinRows = widgetPhoneMinRows(def);
        const phoneHpx = phoneRows * rowHeight;
        return (
          <div
            key={widget.id}
            className={`dashboard-widget${layoutEdit ? ' dashboard-widget-editing' : ''}${draggingId === widget.id ? ' dashboard-widget-dragging' : ''}`}
            style={isPhone ? {
              '--dash-widget-min-h': `${phoneHpx}px`,
              '--dash-widget-h': `${phoneHpx}px`,
              minHeight: `${phoneHpx}px`,
              height: `${phoneHpx}px`,
            } : {
              gridColumn: `${widget.x + 1} / span ${widget.w}`,
              gridRow: `${widget.y + 1} / span ${widget.h}`,
            }}
            onPointerDown={layoutEdit && (!isPinned || canEditPinnedWidgets) ? (e) => {
              if (e.target.closest('button, a, input, select, textarea, .dashboard-widget-resize, .chart-filter-popover, .app-select, .app-select-popover')) return;
              startDrag(widget.id, e);
            } : undefined}
          >
            {layoutEdit && (
              <div className="dashboard-widget-chrome">
                <span className="dashboard-widget-label">{def.label || widget.id}</span>
                {inspectEnabled && (
                  <button
                    type="button"
                    className="dashboard-widget-inspect-chrome"
                    onClick={() => onInspectWidget?.(widget.id)}
                    aria-label={`Inspeção ${def.label || widget.id}`}
                    title="Inspeção (Admin)"
                  >
                    <Icon name="eye" size={14} />
                  </button>
                )}
                {isPhone && (!isPinned || canEditPinnedWidgets) && (
                  <>
                    <button
                      type="button"
                      className="dashboard-widget-move"
                      disabled={stackedWidgets[0]?.id === widget.id}
                      onClick={() => shiftPhoneWidget(widget.id, -1)}
                      aria-label={`Subir ${def.label || widget.id}`}
                      title="Subir"
                    >
                      <Icon name="chevronUp" size={14} />
                    </button>
                    <button
                      type="button"
                      className="dashboard-widget-move"
                      disabled={stackedWidgets[phoneCount - 1]?.id === widget.id}
                      onClick={() => shiftPhoneWidget(widget.id, 1)}
                      aria-label={`Descer ${def.label || widget.id}`}
                      title="Descer"
                    >
                      <Icon name="chevronDown" size={14} />
                    </button>
                    <button
                      type="button"
                      className="dashboard-widget-move"
                      disabled={phoneRows <= phoneMinRows}
                      onClick={() => bumpPhoneWidgetH(widget.id, -1)}
                      aria-label={`Diminuir ${def.label || widget.id}`}
                      title="Diminuir altura"
                    >
                      <Icon name="minus" size={14} />
                    </button>
                    <button
                      type="button"
                      className="dashboard-widget-move"
                      disabled={phoneRows >= PHONE_WIDGET_MAX_H}
                      onClick={() => bumpPhoneWidgetH(widget.id, 1)}
                      aria-label={`Aumentar ${def.label || widget.id}`}
                      title="Aumentar altura"
                    >
                      <Icon name="plus" size={14} />
                    </button>
                  </>
                )}
                {(!isPinned || canEditPinnedWidgets) && (
                  <button
                    type="button"
                    className="dashboard-widget-drag"
                    onPointerDown={e => startDrag(widget.id, e)}
                    aria-label={`Mover ${def.label || widget.id}`}
                  >
                    <Icon name="bars" size={14} />
                  </button>
                )}
            {!def.pinned && (
                  <button
                    type="button"
                    className="dashboard-widget-remove"
                    onClick={() => removeWidget(widget.id)}
                    aria-label={`Remover ${def.label || widget.id}`}
                  >
                    <Icon name="close" size={12} />
                  </button>
                )}
              </div>
            )}
            <div className={`dashboard-widget-content${layoutEdit && isPinned && !canEditPinnedWidgets ? ' is-widget-locked' : ''}`}>
              {renderWidget(widget.id, widget)}
            </div>
            {layoutEdit && (
              <button
                type="button"
                className="dashboard-widget-resize"
                onPointerDown={e => startResize(widget.id, e)}
                aria-label={`Redimensionar ${def.label || widget.id}`}
                style={{ display: (isPinned && !canEditPinnedWidgets) ? 'none' : undefined }}
              />
            )}
          </div>
        );
      })}
    </div>
  );
}

const KPI_PLACEHOLDERS = [
  { label: 'Negócios em Pipeline', value: null, changePercent: null, format: 'number' },
  { label: 'Valor Total', value: null, changePercent: null, format: 'currency' },
  { label: 'Negócios Ganhos', value: null, changePercent: null, format: 'number' },
  { label: 'Comissão Estimada', value: null, changePercent: null, format: 'currency' },
];

function EstadoBucketsCard({ title, buckets, loading, emptyLabel = 'Sem dados.', icon = 'graph', onExplore = null, period = '' }) {
  const rows = Array.isArray(buckets) ? buckets : [];
  return (
    <DashCard className="dash-stat-card" icon={icon} title={title} period={period} onExplore={onExplore}>
      {loading ? (
        <CardLoading message="" />
      ) : rows.length === 0 ? (
        <p className="empty-hint">{emptyLabel}</p>
      ) : (
        <div className="dash-stat-rows">
          {rows.map(b => {
            const estado = b.estado ?? b.Estado ?? '—';
            const count = b.count ?? b.Count ?? 0;
            const montante = b.montante ?? b.Montante ?? 0;
            return (
              <div key={estado} className="dash-stat-row">
                <span className={`status-badge ${statusClass(estado)}`}>{estado}</span>
                <strong className="dash-stat-count">{count}</strong>
                <span className="dash-stat-money">{formatCurrency(montante)}</span>
              </div>
            );
          })}
        </div>
      )}
    </DashCard>
  );
}

function PipelineAltoCard({ count, montante, loading }) {
  return (
    <div className="kpi-card-v2">
      <div className="kpi-card-v2-icon" aria-hidden="true"><Icon name="graph" size={18} /></div>
      {loading ? (
        <CardLoading message="" />
      ) : (
        <>
          <div className="kpi-card-v2-body">
            <span className="kpi-card-v2-label">Pipeline Em Curso Alto</span>
            <div className="kpi-card-v2-value">{count ?? 0}</div>
          </div>
          <div className="kpi-card-v2-footer">
            <div className="kpi-trend neutral">
              {formatCurrency(montante ?? 0)} · Alta
            </div>
          </div>
        </>
      )}
    </div>
  );
}

const DASH_MONTH_SHORT = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'];

function dashboardPeriodLabel(ano = '', mes = '') {
  const monthNum = Number(mes);
  const monthName = monthNum >= 1 && monthNum <= 12 ? DASH_MONTH_SHORT[monthNum - 1] : '';
  if (monthName && ano) return `${monthName} ${ano}`;
  if (monthName) return monthName;
  if (ano) return String(ano);
  return '';
}

function dashboardMonthScopeLabel(ano = '', mes = '') {
  return dashboardPeriodLabel(ano, mes);
}

function OperacaoBarsCard({ ano = '', mes = '', title, metric = 'count', estados = [], probabilidades = [], groupedNatureza = false, stacked = false, showProbFilter = true, layout = 'bars', icon = '', onExplore = null }) {
  const [rows, setRows] = useState([]);
  const [loading, setLoading] = useState(true);
  const [probFilter, setProbFilter] = useState(probabilidades);

  useEffect(() => {
    let cancelled = false;
    const load = async () => {
      setLoading(true);
      try {
        const fetchSlice = async (naturezas) => {
          const params = new URLSearchParams();
          if (ano) params.set('ano', String(ano));
          if (mes) params.set('mes', String(mes));
          params.set('metric', metric);
          if (naturezas.length) params.set('naturezas', naturezas.join(','));
          if (estados.length) params.set('estados', estados.join(','));
          if (probFilter.length) params.set('probabilidades', probFilter.join(','));
          const res = await apiFetch(`/api/dashboard/comissao-por-operacao?${params}`);
          return res.items || res.Items || [];
        };
        let next;
        if (groupedNatureza) {
          const [venda, arrend] = await Promise.all([
            fetchSlice(['Venda']),
            fetchSlice(['Arrendamento']),
          ]);
          const nameOf = (i) => i.operacao || i.Operacao;
          const names = new Set([...venda.map(nameOf), ...arrend.map(nameOf)]);
          next = [...names].filter(Boolean).map(label => ({
            label,
            venda: Number((venda.find(i => nameOf(i) === label) || {}).total
              ?? (venda.find(i => nameOf(i) === label) || {}).Total
              ?? 0),
            arrendamento: Number((arrend.find(i => nameOf(i) === label) || {}).total
              ?? (arrend.find(i => nameOf(i) === label) || {}).Total
              ?? 0),
          }));
        } else {
          const items = await fetchSlice(['Venda', 'Arrendamento']);
          next = items.map(i => ({
            label: i.operacao || i.Operacao || '—',
            total: Number(i.total ?? i.Total ?? 0),
          }));
        }
        if (!cancelled) setRows(next);
      } catch {
        if (!cancelled) setRows([]);
      } finally {
        if (!cancelled) setLoading(false);
      }
    };
    load();
    return () => { cancelled = true; };
  }, [ano, mes, metric, estados.join('|'), probFilter.join('|'), groupedNatureza]);

  const max = Math.max(1, ...rows.map(r => groupedNatureza
    ? Math.max(r.venda || 0, r.arrendamento || 0)
    : (r.total || 0)));
  const formatValue = (n) => metric === 'count'
    ? new Intl.NumberFormat('pt-PT').format(n || 0)
    : formatCurrency(n || 0);

  const headIcon = icon || (layout === 'numbers' ? 'home' : 'chartHbar');
  const isNumbers = layout === 'numbers' && !groupedNatureza;
  const periodText = dashboardMonthScopeLabel(ano, mes);
  const hasData = rows.some(r => groupedNatureza
    ? (r.venda || 0) > 0 || (r.arrendamento || 0) > 0
    : (r.total || 0) > 0);

  return (
    <DashCard
      className={`dash-bars-card${isNumbers ? ' dash-count-card' : ''}`}
      icon={headIcon}
      title={title}
      period={periodText}
      onExplore={onExplore}
      actions={showProbFilter && !groupedNatureza
        ? <ProbabilidadeMultiSelect value={probFilter} onChange={setProbFilter} />
        : null}
    >
      {loading ? <CardLoading /> : !hasData ? (
        <p className="empty-hint">Sem dados.</p>
      ) : isNumbers ? (
        <div className="dash-count-grid">
          {rows.map(r => (
            <div className="dash-count-tile" key={r.label}>
              <span className="dash-count-label">{r.label}</span>
              <strong className="dash-count-value">{formatValue(r.total)}</strong>
            </div>
          ))}
        </div>
      ) : (
        <div className="dash-bars">
          {rows.map(r => (
            <div className="dash-bar-row" key={r.label}>
              <span className="dash-bar-label">{r.label}</span>
              {groupedNatureza ? (
                <div className="dash-bar-pair">
                  <AppTooltip label={`Venda ${formatValue(r.venda)}`}>
                    <div className="dash-bar-track">
                      <div className="dash-bar-fill is-venda" style={{ width: `${(r.venda / max) * 100}%` }} />
                    </div>
                  </AppTooltip>
                  <AppTooltip label={`Arrendamento ${formatValue(r.arrendamento)}`}>
                    <div className="dash-bar-track">
                      <div className="dash-bar-fill is-arrend" style={{ width: `${(r.arrendamento / max) * 100}%` }} />
                    </div>
                  </AppTooltip>
                </div>
              ) : (
                <div className="dash-bar-track">
                  <div className="dash-bar-fill" style={{ width: `${((r.total || 0) / max) * 100}%` }} />
                </div>
              )}
              <strong className="dash-bar-value">
                {groupedNatureza ? formatValue((r.venda || 0) + (r.arrendamento || 0)) : formatValue(r.total)}
              </strong>
            </div>
          ))}
          {groupedNatureza && (
            <div className="dash-bar-legend">
              <span><i className="dash-swatch is-venda" /> Venda</span>
              <span><i className="dash-swatch is-arrend" /> Arrendamento</span>
            </div>
          )}
        </div>
      )}
    </DashCard>
  );
}

async function loadAllListItems(listKey) {
  const items = [];
  let token = '';
  for (let page = 0; page < 20; page += 1) {
    const params = new URLSearchParams({ pageSize: '200' });
    if (token) params.set('pageToken', token);
    const res = await apiFetch(`/api/lists/${listKey}/items?${params}`);
    const rows = res.items || res.Items || [];
    items.push(...rows);
    token = res.nextPageToken || res.NextPageToken || '';
    if (!token) break;
  }
  return items;
}

function listItemFields(item) {
  return item?.fields || item?.Fields || item || {};
}

function listItemYear(fields, fallbackYear) {
  const ano = Number(fields.Ano ?? fields.ano);
  if (Number.isFinite(ano) && ano >= 2000 && ano <= 2100) return ano;
  const raw = fields.Data || fields.data || fields.Created;
  const fromDate = raw ? new Date(raw).getFullYear() : NaN;
  if (Number.isFinite(fromDate) && fromDate >= 2000) return fromDate;
  return fallbackYear ?? null;
}

function listItemMoney(fields) {
  const raw = fields.valor ?? fields.Valor ?? fields.montante ?? fields.Montante ?? fields.valor_comissao ?? 0;
  if (typeof raw === 'number') return raw;
  let text = String(raw).replace(/[€\s\u00A0]/g, '');
  if (text.includes(',') && text.includes('.')) text = text.replace(/\./g, '').replace(',', '.');
  else if (text.includes(',')) text = text.replace(',', '.');
  const n = Number(text);
  return Number.isFinite(n) ? n : 0;
}

function listItemPedidoId(fields) {
  return String(fields.ID_pedidoLookupId ?? fields.id_pedidoLookupId ?? fields.ID_pedido ?? fields.Id_pedido ?? '').replace(/^#/, '');
}

function isFaturadoNao(fields) {
  const v = String(fields.faturado ?? fields.Faturado ?? '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
  return v === 'nao';
}

function DashCardIcon({ name }) {
  return (
    <span className="kpi-card-v2-icon dash-card-head-icon" aria-hidden="true">
      <Icon name={name} size={16} />
    </span>
  );
}

function dashCardTitleText(title) {
  if (title == null || title === false) return '';
  if (typeof title === 'string' || typeof title === 'number') return String(title).trim();
  const parts = [];
  const walk = (node) => {
    if (node == null || node === false || typeof node === 'boolean') return;
    if (typeof node === 'string' || typeof node === 'number') {
      const text = String(node).trim();
      if (text) parts.push(text);
      return;
    }
    if (Array.isArray(node)) {
      node.forEach(walk);
      return;
    }
    if (node.props?.children != null) walk(node.props.children);
  };
  walk(title);
  return parts.join(' ').replace(/\s+/g, ' ').trim();
}

function DashCardTitle({ icon, className = '', children }) {
  return (
    <h3 className={`kpi-card-v2-head${className ? ` ${className}` : ''}`}>
      {icon ? <DashCardIcon name={icon} /> : null}
      <span className="kpi-card-v2-label">{children}</span>
    </h3>
  );
}

function dashCardNestedInteractive(target, current) {
  const el = target instanceof Element ? target : target?.parentElement;
  if (!el || !current) return false;
  const hit = el.closest('button, a, input, select, textarea, label, [role="tab"], [role="menuitem"], .app-select, .app-select-popover');
  return Boolean(hit && hit !== current);
}

function DashCard({
  icon,
  title,
  titleTip,
  actions = null,
  className = '',
  children,
  onClick,
  role,
  tabIndex,
  onKeyDown,
  onExplore = null,
  exploreLabel = 'Explorar',
  disableCompact = false,
  period = '',
}) {
  const cardRef = useRef(null);
  const labelRef = useRef(null);
  const compactRef = useRef(false);
  const [compact, setCompact] = useState(false);
  const tip = titleTip || dashCardTitleText(title);
  const hasTitle = title != null && title !== '';
  const periodText = String(period || '').trim();
  const explorable = typeof onExplore === 'function';
  const clickable = explorable || typeof onClick === 'function';
  const showFoot = explorable || Boolean(periodText);

  const handleCardClick = (e) => {
    if (!clickable) return;
    if (dashCardNestedInteractive(e.target, e.currentTarget)) return;
    if (typeof onClick === 'function') onClick(e);
    else onExplore();
  };

  const handleCardKeyDown = (e) => {
    if (e.key !== 'Enter' && e.key !== ' ') {
      onKeyDown?.(e);
      return;
    }
    if (dashCardNestedInteractive(e.target, e.currentTarget)) {
      onKeyDown?.(e);
      return;
    }
    if (typeof onKeyDown === 'function') {
      onKeyDown(e);
      return;
    }
    if (!clickable) return;
    e.preventDefault();
    if (typeof onClick === 'function') onClick(e);
    else onExplore();
  };

  useLayoutEffect(() => {
    const card = cardRef.current;
    const label = labelRef.current;
    if (!card || !label) {
      compactRef.current = false;
      setCompact(false);
      return undefined;
    }
    const head = card.querySelector('.dash-card-v2-head');
    if (!head) return undefined;
    let frame = 0;
    const apply = (next) => {
      if (compactRef.current === next) return;
      compactRef.current = next;
      setCompact(next);
    };
    const measure = () => {
      cancelAnimationFrame(frame);
      frame = requestAnimationFrame(() => {
        if (!labelRef.current || !head.isConnected) return;
        const iconEl = head.querySelector('.dash-card-head-icon');
        const actionsEl = head.querySelector('.dash-card-v2-actions');
        const room = Math.max(0, head.clientWidth - (iconEl?.offsetWidth || 0) - (actionsEl?.offsetWidth || 0) - 8);
        const probe = labelRef.current.cloneNode(true);
        probe.className = 'kpi-card-v2-label';
        probe.style.cssText = 'position:absolute;left:0;top:0;visibility:hidden;display:inline-flex;width:auto;max-width:none;height:auto;white-space:nowrap;flex:none;opacity:0;pointer-events:none;clip:auto;clip-path:none;margin:0;';
        card.appendChild(probe);
        const needed = probe.offsetWidth;
        probe.remove();
        const tooShort = !disableCompact && card.clientHeight > 0 && card.clientHeight <= 100;
        const isCompact = compactRef.current;
        if (disableCompact) {
          apply(false);
          return;
        }
        if (tooShort) {
          apply(true);
          return;
        }
        if (!isCompact && needed > room) apply(true);
        else if (isCompact && needed <= room - 24) apply(false);
      });
    };
    const ro = new ResizeObserver(measure);
    ro.observe(head);
    measure();
    return () => {
      cancelAnimationFrame(frame);
      ro.disconnect();
    };
  }, [tip, hasTitle, disableCompact]);

  const iconNode = icon ? <DashCardIcon name={icon} /> : null;
  const labeledIcon = iconNode && tip
    ? (
      <AppTooltip label={tip} className="dash-card-title-tip">
        {iconNode}
      </AppTooltip>
    )
    : iconNode;

  return (
    <div
      ref={cardRef}
      className={`dashboard-widget-card dash-card-v2${compact ? ' is-title-compact' : ''}${clickable ? ' clickable' : ''}${className ? ` ${className}` : ''}`}
      onClick={clickable ? handleCardClick : undefined}
      role={role || (clickable ? 'button' : undefined)}
      tabIndex={tabIndex ?? (clickable ? 0 : undefined)}
      onKeyDown={clickable || onKeyDown ? handleCardKeyDown : undefined}
    >
      {(icon || hasTitle || actions) ? (
        <div className="dash-card-v2-head">
          <div className="kpi-card-v2-head">
            {labeledIcon}
            {hasTitle ? (
              <span ref={labelRef} className="kpi-card-v2-label" aria-hidden={compact}>
                {title}
              </span>
            ) : null}
          </div>
          {actions ? <div className="dash-card-v2-actions">{actions}</div> : null}
        </div>
      ) : null}
      <div className="dash-card-v2-body">{children}</div>
      {showFoot ? (
        <div className={`dash-card-v2-explore${periodText ? ' has-period' : ''}${explorable ? ' has-explore' : ''}`}>
          {periodText ? <span className="dash-card-period-foot">{periodText}</span> : null}
          {explorable ? (
            <button
              type="button"
              className="kpi-card-v2-link dash-card-explore"
              onClick={(e) => {
                e.stopPropagation();
                onExplore();
              }}
            >
              {exploreLabel}
            </button>
          ) : null}
        </div>
      ) : null}
    </div>
  );
}

function compactEuro(n) {
  const v = Number(n) || 0;
  if (Math.abs(v) >= 1000000) {
    return `${new Intl.NumberFormat('pt-PT', { maximumFractionDigits: 1 }).format(v / 1000000)} M€`;
  }
  if (Math.abs(v) >= 10000) {
    return `${new Intl.NumberFormat('pt-PT', { maximumFractionDigits: 0 }).format(v / 1000)} mil €`;
  }
  return formatCurrency(v);
}

const FATURACAO_VALIDADO_OPTIONS = ['Aprovado', 'Pendente', 'Reprovado'];
const FATURACAO_ORIGEM_OPTIONS = [
  { value: 'faturacao', label: 'Faturação' },
  { value: 'referencia', label: 'Referência' },
  { value: 'partilha', label: 'Partilha' },
];
const FATURACAO_CHART_DEFAULTS = {
  validados: ['Aprovado'],
  origens: ['faturacao', 'referencia', 'partilha'],
  operacoes: [],
};

function listItemValidado(fields) {
  return String(fields.validado ?? fields.Validado ?? fields.aprovado ?? fields.Aprovado ?? '').trim();
}

function matchesFaturacaoValidado(fields, pedidoFields, validados) {
  if (!validados || !validados.length) return true;
  const value = listItemValidado(fields) || (pedidoFields ? listItemValidado(pedidoFields) : '');
  if (!value) return false;
  return validados.some(v => String(v).toLowerCase() === value.toLowerCase());
}

function buildFaturacaoChartsFromLists(faturacao, pedidos, referencias, partilhas, year, validados, origens) {
  const pedidoById = new Map();
  pedidos.forEach(p => {
    const id = String(p.id || p.Id || '');
    if (id) pedidoById.set(id, listItemFields(p));
  });
  const yearOf = (fields) => {
    const pedido = pedidoById.get(listItemPedidoId(fields));
    const pedidoYear = pedido ? listItemYear(pedido) : null;
    return listItemYear(fields, pedidoYear);
  };
  const operacaoOf = (fields) => {
    const pedido = pedidoById.get(listItemPedidoId(fields));
    const fromPedido = pedido && (pedido.local || pedido.Local || pedido.Operação || pedido.Operacao);
    const own = fields.local || fields.Local || fields.Operação || fields.Operacao;
    return String(fromPedido || own || '(sem operação)');
  };
  const include = (key) => !origens?.length || origens.includes(key);
  const fatMap = {};
  if (include('faturacao')) {
    faturacao.forEach(item => {
      const fields = listItemFields(item);
      if (isFaturadoNao(fields)) return;
      const pedido = pedidoById.get(listItemPedidoId(fields));
      if (!matchesFaturacaoValidado(fields, pedido, validados)) return;
      if (year && yearOf(fields) !== Number(year)) return;
      let valor = listItemMoney(fields);
      if (valor <= 0) valor = pedido ? listItemMoney(pedido) : 0;
      if (valor <= 0) return;
      const op = operacaoOf(fields);
      fatMap[op] = (fatMap[op] || 0) + valor;
    });
  }
  const accumulate = (rows) => {
    const map = {};
    rows.forEach(item => {
      const fields = listItemFields(item);
      const pedido = pedidoById.get(listItemPedidoId(fields));
      if (!matchesFaturacaoValidado(fields, pedido, validados)) return;
      if (year && yearOf(fields) !== Number(year)) return;
      const valor = listItemMoney(fields);
      if (valor <= 0) return;
      const op = operacaoOf(fields);
      map[op] = (map[op] || 0) + valor;
    });
    return map;
  };
  const refMap = include('referencia') ? accumulate(referencias) : {};
  const partMap = include('partilha') ? accumulate(partilhas) : {};
  const ops = [...new Set([...Object.keys(fatMap), ...Object.keys(refMap), ...Object.keys(partMap)])];
  const operacao = ops.map(label => {
    const faturacaoAmt = fatMap[label] || 0;
    const referencia = refMap[label] || 0;
    const partilhaAmt = partMap[label] || 0;
    return {
      label,
      operacao: label,
      faturacao: faturacaoAmt,
      referencia,
      partilha: partilhaAmt,
      total: faturacaoAmt + referencia + partilhaAmt,
    };
  }).filter(r => r.total > 0).sort((a, b) => b.total - a.total);
  const split = [];
  [...new Set([...Object.keys(refMap), ...Object.keys(partMap)])].sort().forEach(op => {
    if (refMap[op] > 0) split.push({ label: 'Referência', total: refMap[op], operacao: op });
    if (partMap[op] > 0) split.push({ label: 'Partilha', total: partMap[op], operacao: op });
  });
  const totais = {
    faturacao: Object.values(fatMap).reduce((s, n) => s + n, 0),
    referencia: Object.values(refMap).reduce((s, n) => s + n, 0),
    partilha: Object.values(partMap).reduce((s, n) => s + n, 0),
  };
  return { byOperacao: operacao, partilha: split, total: totais.faturacao + totais.referencia + totais.partilha, totais };
}

function FaturacaoChartsCard({ ano = '', mes = '', onExplore = null }) {
  const defaults = FATURACAO_CHART_DEFAULTS;
  const [draft, setDraft] = useState(() => ({
    validados: [...defaults.validados],
    origens: [...defaults.origens],
    operacoes: [],
  }));
  const [applied, setApplied] = useState(() => ({
    validados: [...defaults.validados],
    origens: [...defaults.origens],
    operacoes: [],
  }));
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [showFilters, setShowFilters] = useState(false);
  const [popoverPos, setPopoverPos] = useState(null);
  const filterBtnRef = useRef(null);
  const popoverRef = useRef(null);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    const params = new URLSearchParams();
    if (ano) params.set('ano', String(ano));
    if (mes) params.set('mes', String(mes));
    params.set('validados', applied.validados.join(','));
    params.set('origens', applied.origens.join(','));
    (async () => {
      try {
        const fromApi = await apiFetch(`/api/dashboard/faturacao-charts?${params}`).catch(() => null);
        const apiSlices = fromApi?.byOperacao || fromApi?.ByOperacao;
        if (Array.isArray(apiSlices)) {
          if (!cancelled) setData(fromApi);
          return;
        }
        const [faturacao, pedidos, referencias, partilhas] = await Promise.all([
          loadAllListItems('faturacao'),
          loadAllListItems('pedidos'),
          loadAllListItems('referencias'),
          loadAllListItems('partilhas'),
        ]);
        if (!cancelled) {
          setData(buildFaturacaoChartsFromLists(
            faturacao, pedidos, referencias, partilhas, ano, applied.validados, applied.origens,
          ));
        }
      } catch {
        if (!cancelled) setData(null);
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, [ano, mes, applied.validados.join('|'), applied.origens.join('|')]);

  const byOp = (data?.byOperacao || data?.ByOperacao || []).map(s => ({
    label: s.label || s.Label || '—',
    operacao: s.operacao || s.Operacao || s.label || s.Label || '',
    total: Number(s.total ?? s.Total ?? 0),
    faturacao: Number(s.faturacao ?? s.Faturacao ?? 0),
    referencia: Number(s.referencia ?? s.Referencia ?? 0),
    partilha: Number(s.partilha ?? s.Partilha ?? 0),
  }));
  const ops = [...new Set(byOp.map(s => s.label).filter(Boolean))];
  const rows = applied.operacoes.length
    ? byOp.filter(s => applied.operacoes.includes(s.label))
    : byOp;
  const totaisSrc = data?.totais || data?.Totais || {};
  const totais = {
    faturacao: Number(totaisSrc.faturacao ?? totaisSrc.Faturacao ?? rows.reduce((s, r) => s + r.faturacao, 0)),
    referencia: Number(totaisSrc.referencia ?? totaisSrc.Referencia ?? rows.reduce((s, r) => s + r.referencia, 0)),
    partilha: Number(totaisSrc.partilha ?? totaisSrc.Partilha ?? rows.reduce((s, r) => s + r.partilha, 0)),
  };
  const total = rows.reduce((s, r) => s + r.total, 0);
  const toggle = (list, value) => (list.includes(value) ? list.filter(v => v !== value) : [...list, value]);
  const filterCount = countAppliedFilters(applied, FATURACAO_CHART_DEFAULTS);
  const periodText = dashboardMonthScopeLabel(ano, mes);
  const validadoTag = applied.validados.length && applied.validados.length < FATURACAO_VALIDADO_OPTIONS.length
    ? applied.validados.join(' · ')
    : '';

  const openPopover = useCallback(() => {
    const btn = filterBtnRef.current;
    if (!btn) return;
    const r = btn.getBoundingClientRect();
    setPopoverPos({
      left: Math.max(12, Math.min(r.left, window.innerWidth - 372)),
      top: Math.min(r.bottom + 8, window.innerHeight - 420),
    });
    setShowFilters(true);
  }, []);
  const closePopover = useCallback(() => setShowFilters(false), []);

  useEffect(() => {
    if (!showFilters) return undefined;
    const onDoc = (e) => {
      if (popoverRef.current?.contains(e.target) || filterBtnRef.current?.contains(e.target)) return;
      closePopover();
    };
    const onKey = (e) => { if (e.key === 'Escape') closePopover(); };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDoc);
      document.removeEventListener('keydown', onKey);
    };
  }, [showFilters, closePopover]);

  return (
    <DashCard
      className={`chart-card dash-bars-card dash-faturacao-card${showFilters ? ' filters-open' : ''}`}
      icon="chartPie"
      title={validadoTag ? (
        <>
          Valor total faturação
          <span className="operacao-chart-metric-tag">{validadoTag}</span>
        </>
      ) : 'Valor total faturação'}
      period={periodText}
      onExplore={onExplore}
      actions={(
        <AppTooltip label="Filtros">
          <button
            type="button"
            className={`btn-filter-toggle btn-filter-chart${showFilters ? ' active' : ''}`}
            aria-pressed={showFilters}
            aria-label="Filtros da faturação"
            ref={filterBtnRef}
            onClick={() => (showFilters ? closePopover() : openPopover())}
          >
            <Icon name="filter" size={14} />
            {filterCount > 0 ? <span className="operacao-chart-filter-badge">{filterCount}</span> : null}
          </button>
        </AppTooltip>
      )}
    >
      {loading ? <CardLoading /> : total <= 0 ? (
        <p className="empty-hint">Sem dados.</p>
      ) : (
        <div className="dash-faturacao-body">
          <div className="dash-faturacao-hero">
            <div className="kpi-card-v2-value dash-stat-hero">{formatCurrency(total)}</div>
          </div>
          <div className="dash-faturacao-pies">
            {FATURACAO_ORIGEM_OPTIONS.filter(o => applied.origens.includes(o.value)).map(origem => {
              const field = origem.value;
              const amount = totais[field] || 0;
              const slices = slicesFromRows(rows, field, (label, idx) => {
                const i = ops.indexOf(label);
                return OPERACAO_PIE_COLORS[(i >= 0 ? i : idx) % OPERACAO_PIE_COLORS.length];
              });
              return (
                <div key={origem.value} className="dash-faturacao-pie">
                  <DashMiniPie slices={slices} />
                  <AppTooltip label={`${origem.label} ${formatCurrency(amount)}`}>
                    <span className="dash-faturacao-pie-cap">
                      <span className="dash-faturacao-pie-name">{origem.label}</span>
                      <strong className="dash-faturacao-pie-amt">{compactEuro(amount)}</strong>
                    </span>
                  </AppTooltip>
                </div>
              );
            })}
          </div>
          <div className="dash-bar-legend dash-faturacao-legend">
            {ops.map((op, idx) => {
              const row = rows.find(r => r.label === op);
              const tip = row
                ? `${op} · Faturação ${formatCurrency(row.faturacao)} · Referência ${formatCurrency(row.referencia)} · Partilha ${formatCurrency(row.partilha)}`
                : op;
              return (
                <AppTooltip key={op} label={tip}>
                  <span className="dash-faturacao-legend-item">
                    <i className="dash-swatch" style={{ background: OPERACAO_PIE_COLORS[idx % OPERACAO_PIE_COLORS.length] }} />
                    {op}
                  </span>
                </AppTooltip>
              );
            })}
          </div>
        </div>
      )}
      {showFilters && popoverPos && ReactDOM.createPortal(
        <aside
          ref={popoverRef}
          className="chart-filter-popover"
          aria-label="Filtros da faturação"
          style={{ left: popoverPos.left, top: popoverPos.top }}
        >
          <div className="chart-filter-popover-header">
            <div className="chart-filter-popover-title">Filtro</div>
          </div>
          <div className="chart-filter-popover-body">
            <div className="checkbox-group">
              <span className="checkbox-group-title">Validado</span>
              <div className="checkbox-group-chips">
                {FATURACAO_VALIDADO_OPTIONS.map(opt => (
                  <label key={opt} className="avaliacao-check">
                    <input
                      type="checkbox"
                      checked={draft.validados.includes(opt)}
                      onChange={() => setDraft(d => ({ ...d, validados: toggle(d.validados, opt) }))}
                    />
                    {opt}
                  </label>
                ))}
              </div>
            </div>
            <div className="checkbox-group">
              <span className="checkbox-group-title">Origem</span>
              <div className="checkbox-group-chips">
                {FATURACAO_ORIGEM_OPTIONS.map(opt => (
                  <label key={opt.value} className="avaliacao-check">
                    <input
                      type="checkbox"
                      checked={draft.origens.includes(opt.value)}
                      onChange={() => setDraft(d => ({ ...d, origens: toggle(d.origens, opt.value) }))}
                    />
                    {opt.label}
                  </label>
                ))}
              </div>
            </div>
            {ops.length > 0 && (
              <div className="checkbox-group">
                <span className="checkbox-group-title">Operação</span>
                <div className="checkbox-group-chips">
                  {ops.map(opt => (
                    <label key={opt} className="avaliacao-check">
                      <input
                        type="checkbox"
                        checked={draft.operacoes.length === 0 || draft.operacoes.includes(opt)}
                        onChange={() => setDraft(d => {
                          const current = d.operacoes.length ? d.operacoes : ops;
                          const next = toggle(current, opt);
                          return { ...d, operacoes: next.length === ops.length ? [] : next };
                        })}
                      />
                      {opt}
                    </label>
                  ))}
                </div>
              </div>
            )}
          </div>
          <div className="chart-filter-popover-actions">
            <button type="button" className="btn btn-primary btn-sm" onClick={() => { setApplied(draft); setShowFilters(false); }}>Aplicar</button>
            <button
              type="button"
              className="btn btn-secondary btn-sm"
              onClick={() => {
                const cleared = { validados: [...defaults.validados], origens: [...defaults.origens], operacoes: [] };
                setDraft(cleared);
                setApplied(cleared);
                setShowFilters(false);
              }}
            >
              Limpar
            </button>
          </div>
        </aside>,
        document.body
      )}
    </DashCard>
  );
}

function MixNaturezaCard({ ano = '', mes = '', onExplore = null }) {
  const [rows, setRows] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let cancelled = false;
    const load = async () => {
      setLoading(true);
      try {
        const fetchSlice = async (natureza) => {
          const params = new URLSearchParams();
          if (ano) params.set('ano', String(ano));
          if (mes) params.set('mes', String(mes));
          params.set('metric', 'comissao');
          params.set('naturezas', natureza);
          params.set('estados', 'Em Curso,Concluído');
          const res = await apiFetch(`/api/dashboard/comissao-por-operacao?${params}`);
          return res.items || res.Items || [];
        };
        const [venda, arrend] = await Promise.all([fetchSlice('Venda'), fetchSlice('Arrendamento')]);
        const nameOf = (i) => i.operacao || i.Operacao;
        const names = [...new Set([...venda, ...arrend].map(nameOf).filter(Boolean))];
        const next = names.map(label => {
          const v = venda.find(i => nameOf(i) === label) || {};
          const a = arrend.find(i => nameOf(i) === label) || {};
          return {
            label,
            venda: Number(v.total ?? v.Total ?? 0),
            vendaCount: Number(v.count ?? v.Count ?? 0),
            arrendamento: Number(a.total ?? a.Total ?? 0),
            arrendCount: Number(a.count ?? a.Count ?? 0),
          };
        });
        if (!cancelled) setRows(next);
      } catch {
        if (!cancelled) setRows([]);
      } finally {
        if (!cancelled) setLoading(false);
      }
    };
    load();
    return () => { cancelled = true; };
  }, [ano, mes]);

  const compact = (n) => {
    const v = Number(n) || 0;
    if (v >= 1000000) return `${new Intl.NumberFormat('pt-PT', { maximumFractionDigits: 1 }).format(v / 1000000)} M€`;
    if (v >= 1000) return `${new Intl.NumberFormat('pt-PT', { maximumFractionDigits: 0 }).format(v / 1000)} mil €`;
    return formatCurrency(v);
  };

  return (
    <DashCard
      className="dash-bars-card mix-vbar-card"
      icon="chartHbar"
      title="Mix Vendas / Arrendamento"
      period={dashboardPeriodLabel(ano, mes)}
      onExplore={onExplore}
    >
      {loading ? <CardLoading /> : !rows.some(r => (r.venda || 0) > 0 || (r.arrendamento || 0) > 0) ? (
        <p className="empty-hint">Sem dados.</p>
      ) : (
        <div className="mix-vbar-body">
          <div className="mix-compare" role="table" aria-label="Mix vendas e arrendamento">
            <div className="mix-compare-head" role="row">
              <span />
              <span className="is-venda"><i className="dash-swatch is-venda" />Venda</span>
              <span className="is-arrend"><i className="dash-swatch is-arrend" />Arrend.</span>
            </div>
            {rows.map(r => {
              const total = r.venda + r.arrendamento;
              const vendaPct = total > 0 ? (r.venda / total) * 100 : 0;
              const arrendPct = total > 0 ? (r.arrendamento / total) * 100 : 0;
              const tip = `${r.label} · Venda ${formatCurrency(r.venda)} · ${r.vendaCount} · Arrendamento ${formatCurrency(r.arrendamento)} · ${r.arrendCount} · ${ano || 'todos os anos'}`;
              return (
                <AppTooltip key={r.label} label={tip} className="mix-compare-tip">
                  <div className="mix-compare-row" role="row">
                    <span className="mix-vbar-label">{r.label}</span>
                    <strong className="is-venda">{compact(r.venda)}</strong>
                    <strong className="is-arrend">{compact(r.arrendamento)}</strong>
                    <div className="mix-tile-track" aria-hidden="true">
                      <span className="mix-row-fill is-venda" style={{ width: `${vendaPct}%` }} />
                      <span className="mix-row-fill is-arrend" style={{ width: `${arrendPct}%` }} />
                    </div>
                  </div>
                </AppTooltip>
              );
            })}
          </div>
        </div>
      )}
    </DashCard>
  );
}

function ComissaoForecastCard({ ano = '', mes = '', onExplore = null }) {
  const defaults = {
    metric: 'comissao',
    mes: '',
    naturezas: ['Venda', 'Arrendamento'],
    estados: ['Em Curso', 'Concluído'],
    probabilidades: [],
  };
  const [draft, setDraft] = useState(defaults);
  const [applied, setApplied] = useState(defaults);
  const [rows, setRows] = useState([]);
  const [loading, setLoading] = useState(true);
  const [showFilters, setShowFilters] = useState(false);
  const [popoverPos, setPopoverPos] = useState(null);
  const filterBtnRef = useRef(null);

  const loadSlice = useCallback(async (estado) => {
    const params = new URLSearchParams();
    if (ano) params.set('ano', String(ano));
    if (applied.mes || mes) params.set('mes', String(applied.mes || mes));
    params.set('metric', applied.metric || 'comissao');
    params.set('estados', estado);
    if (applied.naturezas.length) params.set('naturezas', applied.naturezas.join(','));
    if (applied.probabilidades.length) params.set('probabilidades', applied.probabilidades.join(','));
    const res = await apiFetch(`/api/dashboard/comissao-por-operacao?${params}`);
    return res.items || res.Items || [];
  }, [ano, mes, applied]);

  useEffect(() => {
    let cancelled = false;
    const run = async () => {
      setLoading(true);
      try {
        const wanted = applied.estados.length ? applied.estados : ['Em Curso', 'Concluído'];
        const slices = await Promise.all(wanted.map(estado => loadSlice(estado).then(items => ({ estado, items }))));
        const byOp = new Map();
        slices.forEach(({ estado, items }) => {
          items.forEach(item => {
            const label = item.operacao || item.Operacao || '—';
            const total = Number(item.total ?? item.Total ?? 0);
            const row = byOp.get(label) || { label, curso: 0, concluido: 0 };
            if (estado === 'Concluído') row.concluido += total;
            else if (estado === 'Em Curso') row.curso += total;
            byOp.set(label, row);
          });
        });
        if (!cancelled) setRows([...byOp.values()]);
      } catch {
        if (!cancelled) setRows([]);
      } finally {
        if (!cancelled) setLoading(false);
      }
    };
    run();
    return () => { cancelled = true; };
  }, [loadSlice, applied]);

  const curso = rows.reduce((sum, r) => sum + r.curso, 0);
  const concluido = rows.reduce((sum, r) => sum + r.concluido, 0);
  const total = curso + concluido;
  const max = Math.max(1, ...rows.map(r => r.curso + r.concluido));
  const money = applied.metric !== 'count';
  const formatValue = (n) => money ? formatCurrency(n) : new Intl.NumberFormat('pt-PT').format(n);
  const filterCount = countAppliedFilters(applied, defaults);
  const toggle = (list, value) => list.includes(value) ? list.filter(v => v !== value) : [...list, value];

  const openPopover = () => {
    const btn = filterBtnRef.current;
    if (!btn) return;
    const r = btn.getBoundingClientRect();
    setPopoverPos({ left: Math.max(12, Math.min(r.left, window.innerWidth - 372)), top: r.bottom + 8 });
    setShowFilters(true);
  };

  return (
    <DashCard
      className="chart-card dash-bars-card"
      icon="chartHbar"
      title="Previsão Comissão Habita"
      period={dashboardPeriodLabel(ano, applied.mes || mes)}
      onExplore={onExplore}
      actions={(
        <button
          type="button"
          className={`btn-filter-toggle btn-filter-chart${showFilters ? ' active' : ''}`}
          aria-pressed={showFilters}
          aria-label="Filtros do gráfico"
          title="Filtros"
          ref={filterBtnRef}
          onClick={() => (showFilters ? setShowFilters(false) : openPopover())}
        >
          <Icon name="filter" size={14} />
          {filterCount > 0 ? <span className="operacao-chart-filter-badge">{filterCount}</span> : null}
        </button>
      )}
    >
      {loading ? <CardLoading /> : (
        <div className="dash-stat-card-body">
          <div className="kpi-card-v2-value dash-stat-hero">{formatValue(total)}</div>
          <p className="dash-stat-meta">
            Concluído {formatValue(concluido)} + Em curso {formatValue(curso)}
          </p>
          <div className="dash-bars">
            {rows.length === 0 && <p className="empty-hint">Sem dados para os filtros selecionados.</p>}
            {rows.map(r => {
              const sum = r.curso + r.concluido || 1;
              return (
                <div key={r.label} className="dash-bar-row">
                  <span className="dash-bar-label">{r.label}</span>
                  <AppTooltip label={`Em curso ${formatValue(r.curso)} · Concluído ${formatValue(r.concluido)}`}>
                    <div className="dash-bar-track">
                      <div className="dash-bar-fill is-pipe" style={{ width: `${(r.curso / max) * 100}%` }} />
                      <div className="dash-bar-fill is-won" style={{ width: `${(r.concluido / max) * 100}%` }} />
                    </div>
                  </AppTooltip>
                  <strong className="dash-bar-value">{formatValue(sum === 1 && r.curso + r.concluido === 0 ? 0 : r.curso + r.concluido)}</strong>
                </div>
              );
            })}
            <div className="dash-bar-legend">
              <span><i className="dash-swatch is-pipe" /> Em curso</span>
              <span><i className="dash-swatch is-won" /> Concluído</span>
            </div>
          </div>
        </div>
      )}
      {showFilters && popoverPos && ReactDOM.createPortal(
        <aside className="chart-filter-popover" style={{ left: popoverPos.left, top: popoverPos.top }}>
          <div className="chart-filter-popover-header"><div className="chart-filter-popover-title">Filtro</div></div>
          <div className="chart-filter-popover-body">
            <div className="chart-filter-row">
              <div className="form-group">
                <label>Métrica</label>
                <AppSelect
                  value={draft.metric}
                  options={OPERACAO_CHART_METRICS}
                  onChange={v => setDraft(d => ({ ...d, metric: v }))}
                  ariaLabel="Métrica"
                />
              </div>
              <div className="form-group">
                <label>Mês</label>
                <AppSelect
                  value={draft.mes}
                  emptyOption="Todos"
                  options={Array.from({ length: 12 }, (_, i) => i + 1).map(m => ({ value: String(m), label: String(m).padStart(2, '0') }))}
                  onChange={v => setDraft(d => ({ ...d, mes: v }))}
                  ariaLabel="Mês"
                />
              </div>
            </div>
            <div className="checkbox-group">
              <span className="checkbox-group-title">Natureza</span>
              <div className="checkbox-group-chips">
                {NATUREZA_CHOICE_OPTIONS.map(opt => (
                  <label key={opt} className="avaliacao-check">
                    <input type="checkbox" checked={draft.naturezas.includes(opt)} onChange={() => setDraft(d => ({ ...d, naturezas: toggle(d.naturezas, opt) }))} />
                    {opt}
                  </label>
                ))}
              </div>
            </div>
            <div className="checkbox-group">
              <span className="checkbox-group-title">Estado</span>
              <div className="checkbox-group-chips">
                {['Em Curso', 'Concluído'].map(opt => (
                  <label key={opt} className="avaliacao-check">
                    <input type="checkbox" checked={draft.estados.includes(opt)} onChange={() => setDraft(d => ({ ...d, estados: toggle(d.estados, opt) }))} />
                    {opt}
                  </label>
                ))}
              </div>
            </div>
            <ProbabilidadeMultiSelect
              variant="chips"
              value={draft.probabilidades}
              onChange={(next) => setDraft(d => ({ ...d, probabilidades: next }))}
            />
          </div>
          <div className="chart-filter-popover-actions">
            <button type="button" className="btn btn-primary btn-sm" onClick={() => { setApplied(draft); setShowFilters(false); }}>Aplicar</button>
            <button type="button" className="btn btn-secondary btn-sm" onClick={() => { setDraft(defaults); setApplied(defaults); setShowFilters(false); }}>Limpar</button>
          </div>
        </aside>,
        document.body
      )}
    </DashCard>
  );
}

const CAL_WEEKDAYS = ['Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb', 'Dom'];
const CAL_HOURS = [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19];
const CAL_SOURCES = [
  { id: 'pessoal', label: 'Pessoal', icon: 'user' },
  { id: 'tarefas', label: 'Tarefas', icon: 'clipboardList' },
  { id: 'aniversarios', label: 'Aniversários', icon: 'star' },
  { id: 'fotografia', label: 'Fotografia', icon: 'camera' },
];
const CAL_CATEGORIES = [
  { id: 'pessoal', label: 'Pessoal', icon: 'user' },
  { id: 'trabalho', label: 'Trabalho', icon: 'investimentos' },
  { id: 'saude', label: 'Saúde', icon: 'activity' },
];
const CAL_WEEK_HOURS = [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18];
const CAL_SLOT_TYPES = new Set(['livre', 'fotografia', 'bloqueado']);

function calStartOfDay(date) {
  const d = new Date(date);
  d.setHours(0, 0, 0, 0);
  return d;
}

function calStartOfWeek(date) {
  const d = calStartOfDay(date);
  d.setDate(d.getDate() - ((d.getDay() + 6) % 7));
  return d;
}

function calSameDay(a, b) {
  return a.getFullYear() === b.getFullYear()
    && a.getMonth() === b.getMonth()
    && a.getDate() === b.getDate();
}

function calEventCoversDay(ev, day) {
  const t = calStartOfDay(day).getTime();
  return t >= calStartOfDay(ev.start).getTime() && t <= calStartOfDay(ev.end).getTime();
}

function isCalSpanEvent(ev) {
  if (!ev?.start || !ev?.end) return false;
  return calStartOfDay(ev.end).getTime() > calStartOfDay(ev.start).getTime();
}

function calSpanPos(ev, day) {
  if (!isCalSpanEvent(ev)) return '';
  const t = calStartOfDay(day).getTime();
  const s = calStartOfDay(ev.start).getTime();
  const e = calStartOfDay(ev.end).getTime();
  if (t === s && t === e) return '';
  if (t === s) return 'start';
  if (t === e) return 'end';
  return 'mid';
}

function calMonthSpanSegments(events, weekDays) {
  const weekStart = calStartOfDay(weekDays[0]).getTime();
  const weekEnd = calStartOfDay(weekDays[6]).getTime();
  const segs = [];
  for (const ev of events) {
    if (!isCalSpanEvent(ev)) continue;
    const s = calStartOfDay(ev.start).getTime();
    const e = calStartOfDay(ev.end).getTime();
    if (e < weekStart || s > weekEnd) continue;
    let startCol = 1;
    let endCol = 8;
    weekDays.forEach((d, i) => {
      const t = calStartOfDay(d).getTime();
      if (t === Math.max(s, weekStart)) startCol = i + 1;
      if (t === Math.min(e, weekEnd)) endCol = i + 2;
    });
    segs.push({
      ev,
      startCol,
      endCol,
      clipStart: s < weekStart,
      clipEnd: e > weekEnd,
    });
  }
  segs.sort((a, b) => a.startCol - b.startCol || (b.endCol - a.startCol) - (a.endCol - a.startCol));
  const laneEnds = [];
  return segs.map((seg) => {
    let lane = laneEnds.findIndex((end) => seg.startCol >= end);
    if (lane < 0) {
      lane = laneEnds.length;
      laneEnds.push(seg.endCol);
    } else {
      laneEnds[lane] = seg.endCol;
    }
    return { ...seg, lane: lane + 1 };
  });
}

function calAt(day, hour, minute = 0) {
  const d = new Date(day);
  d.setHours(hour, minute, 0, 0);
  return d;
}

function calHm(date) {
  return formatHmFromDate(date);
}

function calHourDecimal(date) {
  return date.getHours() + date.getMinutes() / 60;
}

function calWeekEventBox(ev, hours = CAL_HOURS) {
  const first = hours[0];
  const last = first + hours.length;
  const start = Math.min(Math.max(calHourDecimal(ev.start), first), last);
  const end = Math.min(Math.max(calHourDecimal(ev.end), start + 0.25), last);
  const hoursLen = end - start;
  const span = hours.length || 1;
  return {
    top: `calc(${(start - first) / span} * 100% + 1px)`,
    height: `calc(${hoursLen / span} * 100% - 2px)`,
    short: hoursLen < 0.75,
  };
}

function calDateInput(date) {
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}

function calTimeInput(date) {
  return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
}

function calParseDateTime(dateStr, timeStr) {
  const [y, m, d] = String(dateStr).split('-').map(Number);
  const [hh, mm] = String(timeStr || '09:00').split(':').map(Number);
  return new Date(y, (m || 1) - 1, d || 1, hh || 0, mm || 0, 0, 0);
}

function calApiDate(date) {
  return `${calDateInput(date)}T${calTimeInput(date)}:00`;
}

function calStampUtc(date) {
  return new Date(date).toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
}

function calGoogleUrl(ev) {
  const params = new URLSearchParams({
    action: 'TEMPLATE',
    text: ev.title || 'Evento',
    dates: `${calStampUtc(ev.start)}/${calStampUtc(ev.end)}`,
    location: ev.location || '',
  });
  return `https://calendar.google.com/calendar/render?${params.toString()}`;
}

function calOutlookUrl(ev) {
  const params = new URLSearchParams({
    subject: ev.title || 'Evento',
    startdt: new Date(ev.start).toISOString(),
    enddt: new Date(ev.end).toISOString(),
    location: ev.location || '',
    path: '/calendar/action/compose',
    rru: 'addevent',
  });
  return `https://outlook.office.com/calendar/0/deeplink/compose?${params.toString()}`;
}

async function downloadCalendarIcs(id) {
  const headers = {};
  if (window.__myHabitaViewAsEmail) headers['X-View-As-Email'] = window.__myHabitaViewAsEmail;
  const res = await fetch(`/api/calendar/events/${encodeURIComponent(id)}/ics`, { credentials: 'include', headers });
  if (!res.ok) throw new Error('Não foi possível descarregar o evento.');
  const blob = await res.blob();
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'evento.ics';
  document.body.appendChild(a);
  a.click();
  a.remove();
  URL.revokeObjectURL(url);
}

function isCalSlotType(type) {
  return CAL_SLOT_TYPES.has(String(type || '').toLowerCase());
}

function calEventTone(ev) {
  if (ev.sensitiveHidden) return 'ocupado';
  const type = String(ev.type || '').toLowerCase();
  const status = String(ev.status || '').toLowerCase();
  if (type === 'livre' || status === 'livre') return 'livre';
  if (type === 'bloqueado' || status === 'bloqueado') return 'bloqueado';
  if (status === 'pendente' || status === 'reagendar') return 'pendente';
  if (status === 'aprovado') return 'aprovado';
  return ev.category || 'trabalho';
}

function calEventIcon(ev) {
  if (ev.sensitiveHidden) return 'lock';
  const type = String(ev.type || '').toLowerCase();
  const status = String(ev.status || '').toLowerCase();
  if (type === 'livre' || status === 'livre') return 'camera';
  if (type === 'bloqueado' || status === 'bloqueado') return 'lock';
  if (status === 'pendente' || status === 'reagendar') return 'bell';
  if (status === 'aprovado') return 'camera';
  if (ev.sensitive) return 'lock';
  if (ev.calendar === 'aniversarios') return 'star';
  if (ev.calendar === 'fotografia') return 'camera';
  if (ev.category === 'saude') return 'activity';
  if (ev.category === 'pessoal') return 'user';
  if (ev.calendar === 'tarefas') return 'clipboardList';
  return 'calendar';
}

function isCalSampleId(id) {
  return String(id || '').startsWith('sample-');
}

function buildSampleCalendarEvents(anchor) {
  const mon = calStartOfWeek(anchor);
  const at = (offset, h, m = 0) => calAt(new Date(mon.getFullYear(), mon.getMonth(), mon.getDate() + offset), h, m);
  return [
    { id: 'sample-s1', title: 'Reunião com a Joana', start: at(0, 9), end: at(0, 10), location: 'Escritório Habita', calendar: 'pessoal', category: 'trabalho', people: ['Joana Silva'], type: 'evento', status: 'ativo', canEdit: false, sample: true },
    { id: 'sample-s2', title: 'Visita — T2 Parque', start: at(1, 10, 30), end: at(1, 11, 30), location: 'Parque das Nações', calendar: 'tarefas', category: 'trabalho', people: ['Ana Mendes'], type: 'evento', status: 'ativo', canEdit: false, sample: true },
    { id: 'sample-s3', title: 'Almoço de equipa', start: at(2, 13), end: at(2, 14, 30), location: 'Cantina', calendar: 'pessoal', category: 'pessoal', people: ['Joana Silva', 'Ana Mendes'], type: 'evento', status: 'ativo', canEdit: false, sample: true },
    { id: 'sample-s4', title: 'Consulta', start: at(3, 8, 30), end: at(3, 9, 15), location: 'Clínica', calendar: 'pessoal', category: 'saude', people: [], type: 'evento', status: 'ativo', canEdit: false, sensitive: true, sample: true },
    { id: 'sample-s5', title: 'Follow-up angariação', start: at(3, 15), end: at(3, 16), location: 'Online', calendar: 'tarefas', category: 'trabalho', people: ['Lívia Moreira'], type: 'evento', status: 'ativo', canEdit: false, sample: true },
    { id: 'sample-s6', title: 'Aniversário — Pedro', start: at(4, 12), end: at(4, 13), location: '', calendar: 'aniversarios', category: 'pessoal', people: [], type: 'evento', status: 'ativo', canEdit: false, sample: true },
    { id: 'sample-s7', title: 'Pipeline semanal', start: at(4, 16), end: at(4, 17), location: 'Sala 2', calendar: 'tarefas', category: 'trabalho', people: ['Joana Silva', 'Ana Mendes'], type: 'evento', status: 'ativo', canEdit: false, sample: true },
    { id: 'sample-s8', title: 'Open house — T3 Gaia', start: at(1, 8), end: at(3, 19), location: 'Vila Nova de Gaia', calendar: 'tarefas', category: 'trabalho', people: [], type: 'evento', status: 'ativo', canEdit: false, sample: true },
    { id: 'sample-s9', title: 'Call cliente', start: at(4, 9), end: at(4, 10), location: 'Online', calendar: 'pessoal', category: 'trabalho', people: [], type: 'evento', status: 'ativo', canEdit: false, sample: true },
    { id: 'sample-s10', title: 'Reunião SOI', start: at(4, 11), end: at(4, 12), location: 'Sala 1', calendar: 'tarefas', category: 'trabalho', people: [], type: 'evento', status: 'ativo', canEdit: false, sample: true },
    { id: 'sample-s11', title: 'Autoavaliação T3', start: at(0, 8), end: at(4, 18), location: 'MyHABITA', calendar: 'pessoal', category: 'trabalho', people: [], type: 'evento', status: 'ativo', canEdit: false, sample: true, isSystem: true },
  ];
}

function mergeCalendarEvents(stored, samples) {
  const taken = new Set(stored.map(ev => `${ev.start.getFullYear()}-${ev.start.getMonth()}-${ev.start.getDate()}-${ev.start.getHours()}`));
  const extra = samples.filter(ev => !taken.has(`${ev.start.getFullYear()}-${ev.start.getMonth()}-${ev.start.getDate()}-${ev.start.getHours()}`));
  return [...stored, ...extra];
}

function normalizeCalEvent(raw) {
  const type = raw.type || raw.Type || 'evento';
  const start = new Date(raw.start || raw.Start);
  const end = new Date(raw.end || raw.End);
  return {
    id: raw.id || raw.Id,
    type,
    title: raw.title || raw.Title || '',
    start,
    end,
    location: raw.location || raw.Location || raw.imovel || raw.Imovel || '',
    imovel: raw.imovel || raw.Imovel || '',
    calendar: raw.calendar || raw.Calendar || (isCalSlotType(type) ? 'fotografia' : 'pessoal'),
    category: raw.category || raw.Category || 'trabalho',
    people: Array.isArray(raw.people || raw.People) ? [...(raw.people || raw.People)] : [],
    sensitive: !!(raw.sensitive ?? raw.Sensitive),
    sensitiveHidden: !!(raw.sensitiveHidden ?? raw.SensitiveHidden),
    ownerEmail: raw.ownerEmail || raw.OwnerEmail || '',
    ownerName: raw.ownerName || raw.OwnerName || '',
    status: raw.status || raw.Status || '',
    requestedBy: raw.requestedBy || raw.RequestedBy || '',
    proposedStart: raw.proposedStart || raw.ProposedStart ? new Date(raw.proposedStart || raw.ProposedStart) : null,
    proposedEnd: raw.proposedEnd || raw.ProposedEnd ? new Date(raw.proposedEnd || raw.ProposedEnd) : null,
    canEdit: !!(raw.canEdit ?? raw.CanEdit),
    canApprovePhoto: !!(raw.canApprovePhoto ?? raw.CanApprovePhoto),
    canReschedulePhoto: !!(raw.canReschedulePhoto ?? raw.CanReschedulePhoto),
    canAcceptReschedule: !!(raw.canAcceptReschedule ?? raw.CanAcceptReschedule),
    canCancelPhoto: !!(raw.canCancelPhoto ?? raw.CanCancelPhoto),
    canCancelApprovedPhoto: !!(raw.canCancelApprovedPhoto ?? raw.CanCancelApprovedPhoto),
    isSystem: !!(raw.isSystem ?? raw.IsSystem),
    sample: !!(raw.sample ?? raw.Sample),
  };
}

function defaultCalSettings() {
  return {
    photoMinHoursAhead: 2,
    workStart: '09:00',
    workEnd: '18:00',
    weekendsEnabled: false,
    lunchStart: '13:00',
    lunchEnd: '14:00',
    lunchEnabled: true,
  };
}

function normalizeCalSettings(raw) {
  const d = defaultCalSettings();
  const src = raw || {};
  const hours = Number(src.photoMinHoursAhead ?? src.PhotoMinHoursAhead);
  const lunchFlag = src.lunchEnabled ?? src.LunchEnabled;
  return {
    photoMinHoursAhead: Number.isFinite(hours) ? Math.min(72, Math.max(0, hours)) : d.photoMinHoursAhead,
    workStart: src.workStart || src.WorkStart || d.workStart,
    workEnd: src.workEnd || src.WorkEnd || d.workEnd,
    weekendsEnabled: !!(src.weekendsEnabled ?? src.WeekendsEnabled),
    lunchStart: src.lunchStart || src.LunchStart || d.lunchStart,
    lunchEnd: src.lunchEnd || src.LunchEnd || d.lunchEnd,
    lunchEnabled: lunchFlag == null ? d.lunchEnabled : !!lunchFlag,
  };
}

function mapCalendarDoc(doc) {
  const events = (doc?.events || doc?.Events || []).map(normalizeCalEvent);
  return {
    events,
    canManageSlots: !!(doc?.canManageSlots ?? doc?.CanManageSlots),
    settings: normalizeCalSettings(doc?.settings || doc?.Settings),
  };
}

function emptyCalDraft(day, hour = 9, kind = 'event') {
  const start = calAt(day || new Date(), hour);
  const end = calAt(day || new Date(), hour + 1);
  return {
    id: null,
    kind,
    title: kind === 'slot' ? 'Horário livre' : kind === 'photo' ? 'Visita de fotografia' : '',
    date: calDateInput(start),
    endDate: calDateInput(start),
    startTime: calTimeInput(start),
    endTime: calTimeInput(end),
    location: '',
    imovel: '',
    calendar: kind === 'slot' || kind === 'photo' ? 'fotografia' : 'pessoal',
    category: 'trabalho',
    people: [],
    sensitive: false,
    slotSpan: false,
    proposing: false,
    cancelling: false,
    cancelReason: '',
    isSystem: false,
    locked: false,
  };
}

function isCalLivreSlot(ev) {
  if (!ev) return false;
  const type = String(ev.type || '').toLowerCase();
  const status = String(ev.status || '').toLowerCase();
  return type === 'livre' || status === 'livre';
}

function isCalBloqueadoSlot(ev) {
  if (!ev) return false;
  const type = String(ev.type || '').toLowerCase();
  const status = String(ev.status || '').toLowerCase();
  return type === 'bloqueado' || status === 'bloqueado';
}

function isCalAvailabilitySlot(ev) {
  return isCalLivreSlot(ev) || isCalBloqueadoSlot(ev);
}

function calLivreSlotCovering(events, start, end) {
  const t0 = start instanceof Date ? start.getTime() : new Date(start).getTime();
  const t1 = end instanceof Date ? end.getTime() : new Date(end).getTime();
  return (events || []).find(ev => isCalLivreSlot(ev) && ev.start.getTime() <= t0 && ev.end.getTime() >= t1) || null;
}

function calMergeRanges(ranges) {
  const sorted = (ranges || []).filter(r => r.t1 > r.t0).sort((a, b) => a.t0 - b.t0);
  const out = [];
  for (const r of sorted) {
    const last = out[out.length - 1];
    if (last && r.t0 <= last.t1) last.t1 = Math.max(last.t1, r.t1);
    else out.push({ t0: r.t0, t1: r.t1 });
  }
  return out;
}

function calSubtractRanges(bases, cuts) {
  let segs = calMergeRanges(bases);
  for (const cut of calMergeRanges(cuts)) {
    const next = [];
    for (const s of segs) {
      if (cut.t1 <= s.t0 || cut.t0 >= s.t1) {
        next.push(s);
        continue;
      }
      if (cut.t0 > s.t0) next.push({ t0: s.t0, t1: Math.min(cut.t0, s.t1) });
      if (cut.t1 < s.t1) next.push({ t0: Math.max(cut.t1, s.t0), t1: s.t1 });
    }
    segs = next.filter(x => x.t1 > x.t0);
  }
  return segs;
}

function calIntersectRanges(bases, masks) {
  const out = [];
  for (const b of bases || []) {
    for (const m of masks || []) {
      const t0 = Math.max(b.t0, m.t0);
      const t1 = Math.min(b.t1, m.t1);
      if (t1 > t0) out.push({ t0, t1 });
    }
  }
  return calMergeRanges(out);
}

function calPhotoVisitOverlaps(events, start, end, exceptId) {
  const t0 = start instanceof Date ? start.getTime() : new Date(start).getTime();
  const t1 = end instanceof Date ? end.getTime() : new Date(end).getTime();
  const skip = exceptId == null ? '' : String(exceptId);
  return (events || []).some(ev => (
    isCalPhotoVisit(ev)
    && String(ev.id || '') !== skip
    && ev.start.getTime() < t1
    && ev.end.getTime() > t0
  ));
}

function calLivreSegmentsInRange(events, rangeStart, rangeEnd, settings) {
  if (!(rangeEnd > rangeStart)) return [];
  const win = calWorkWindowOn(rangeStart, settings);
  if (!win) return [];
  let t0 = Math.max(rangeStart.getTime(), win.start.getTime());
  let t1 = Math.min(rangeEnd.getTime(), win.end.getTime());
  if (!(t1 > t0)) return [];
  let segs = [{ t0, t1 }];
  const lunch = calLunchWindowOn(rangeStart, settings);
  if (lunch) segs = calSubtractRanges(segs, [{ t0: lunch.start.getTime(), t1: lunch.end.getTime() }]);
  const livre = (events || [])
    .filter(isCalLivreSlot)
    .map(ev => ({ t0: ev.start.getTime(), t1: ev.end.getTime() }));
  segs = calIntersectRanges(segs, livre);
  const busy = (events || [])
    .filter(ev => isCalPhotoVisit(ev) || isCalBloqueadoSlot(ev))
    .map(ev => ({ t0: ev.start.getTime(), t1: ev.end.getTime() }));
  segs = calSubtractRanges(segs, busy);
  const minStart = Math.max(Date.now(), calPhotoEarliestStart(settings).getTime());
  segs = segs
    .map(s => ({ t0: Math.max(s.t0, minStart), t1: s.t1 }))
    .filter(s => s.t1 - s.t0 >= 60 * 1000);
  return segs;
}

function calRangeFullyLivre(events, start, end, settings) {
  const segs = calLivreSegmentsInRange(events, start, end, settings);
  const t0 = start.getTime();
  const t1 = end.getTime();
  return segs.some(s => s.t0 <= t0 + 1000 && s.t1 >= t1 - 1000);
}

function calLivreGradient(segs, hourStart, hourEnd) {
  const span = hourEnd - hourStart;
  if (!(span > 0)) return '';
  const green = 'rgba(45, 156, 72, 0.12)';
  const stops = ['transparent 0%'];
  for (const s of segs) {
    const a = Math.max(0, Math.min(100, ((s.t0 - hourStart) / span) * 100));
    const b = Math.max(0, Math.min(100, ((s.t1 - hourStart) / span) * 100));
    if (b <= a) continue;
    stops.push(`transparent ${a}%`, `${green} ${a}%`, `${green} ${b}%`, `transparent ${b}%`);
  }
  stops.push('transparent 100%');
  return `linear-gradient(to bottom, ${stops.join(', ')})`;
}

function calBloqueadoCovering(events, start, end) {
  const t0 = start instanceof Date ? start.getTime() : new Date(start).getTime();
  const t1 = end instanceof Date ? end.getTime() : new Date(end).getTime();
  return (events || []).find(ev => isCalBloqueadoSlot(ev) && ev.start.getTime() <= t0 && ev.end.getTime() >= t1) || null;
}

function calAvailabilityCovering(events, start, end) {
  return calLivreSlotCovering(events, start, end) || calBloqueadoCovering(events, start, end);
}

function calParseHmParts(raw, fallback = '09:00') {
  const [hh, mm] = String(raw || fallback).split(':').map(Number);
  return {
    h: Number.isFinite(hh) ? hh : 0,
    m: Number.isFinite(mm) ? mm : 0,
  };
}

function calWorkStartHour(settings) {
  return calParseHmParts(settings?.workStart, '09:00').h;
}

function calLunchEnabled(settings) {
  const s = settings || defaultCalSettings();
  return s.lunchEnabled !== false;
}

function calLunchWindowOn(day, settings) {
  const s = settings || defaultCalSettings();
  if (!calLunchEnabled(s)) return null;
  const startP = calParseHmParts(s.lunchStart, '13:00');
  const endP = calParseHmParts(s.lunchEnd, '14:00');
  const start = calAt(day, startP.h, startP.m);
  const end = calAt(day, endP.h, endP.m);
  if (!(end > start)) return null;
  return { start, end };
}

function calRangeOverlapsLunch(start, end, settings) {
  if (!(end > start) || !calLunchEnabled(settings)) return false;
  let cursor = new Date(start);
  while (cursor < end) {
    const lunch = calLunchWindowOn(cursor, settings);
    const nextDay = calStartOfDay(cursor);
    nextDay.setDate(nextDay.getDate() + 1);
    const pieceEnd = end < nextDay ? end : nextDay;
    if (lunch && cursor < lunch.end && pieceEnd > lunch.start) return true;
    cursor = pieceEnd;
  }
  return false;
}

function calWorkHoursCaption(settings) {
  const s = settings || defaultCalSettings();
  const lunch = calLunchEnabled(s) ? `, pausa ${s.lunchStart}–${s.lunchEnd}` : '';
  const days = s.weekendsEnabled ? ', incluindo fins de semana' : ', dias úteis';
  return `${s.workStart}–${s.workEnd}${lunch}${days}`;
}

function calFirstBookableHour(day, settings) {
  const win = calWorkWindowOn(day, settings);
  if (!win) return calWorkStartHour(settings);
  const minStart = calPhotoEarliestStart(settings);
  const from = win.start.getTime() > minStart.getTime() ? win.start : minStart;
  if (!(from < win.end)) return calWorkStartHour(settings);
  let hour = from.getHours();
  if (from.getMinutes() > 0 || from.getSeconds() > 0 || from.getMilliseconds() > 0) hour += 1;
  while (true) {
    const slot = calAt(day, hour);
    if (slot >= win.end) return calWorkStartHour(settings);
    if (!calRangeOverlapsLunch(slot, calAt(day, hour + 1), settings)) return hour;
    hour += 1;
  }
}

function calIsWeekend(day) {
  const d = day instanceof Date ? day.getDay() : new Date(day).getDay();
  return d === 0 || d === 6;
}

function calWorkWindowOn(day, settings) {
  const s = settings || defaultCalSettings();
  if (!s.weekendsEnabled && calIsWeekend(day)) return null;
  const startP = calParseHmParts(s.workStart, '09:00');
  const endP = calParseHmParts(s.workEnd, '18:00');
  const start = calAt(day, startP.h, startP.m);
  const end = calAt(day, endP.h, endP.m);
  if (!(end > start)) return null;
  return { start, end };
}

function calRangeInWorkHours(start, end, settings) {
  if (!(end > start)) return false;
  let cursor = new Date(start);
  while (cursor < end) {
    const win = calWorkWindowOn(cursor, settings);
    if (!win) return false;
    const nextDay = calStartOfDay(cursor);
    nextDay.setDate(nextDay.getDate() + 1);
    const pieceEnd = end < nextDay ? end : nextDay;
    if (cursor < win.start || pieceEnd > win.end) return false;
    cursor = pieceEnd;
  }
  return true;
}

function calPhotoEarliestStart(settings) {
  const hours = Number(settings?.photoMinHoursAhead);
  const n = Number.isFinite(hours) && hours >= 0 ? hours : 2;
  return new Date(Date.now() + n * 3600000);
}

function calPhotoStartAllowed(start, settings) {
  const t = start instanceof Date ? start.getTime() : new Date(start).getTime();
  if (!Number.isFinite(t)) return false;
  if (t <= Date.now()) return false;
  return t >= calPhotoEarliestStart(settings).getTime();
}

function calPhotoTimingReason(start, end, settings, opts) {
  if (!(end > start)) return 'A data e hora de fim têm de ser depois do início.';
  const startMs = start instanceof Date ? start.getTime() : new Date(start).getTime();
  if (startMs <= Date.now()) return 'A visita de fotografia só pode ser pedida no futuro.';
  if (!calPhotoStartAllowed(start, settings)) {
    const n = Number(settings?.photoMinHoursAhead);
    const hours = Number.isFinite(n) && n >= 0 ? n : 2;
    return hours === 1
      ? 'A visita de fotografia só pode ser pedida com 1 hora de antecedência.'
      : `A visita de fotografia só pode ser pedida com ${hours} horas de antecedência.`;
  }
  if (!calRangeInWorkHours(start, end, settings)) {
    return 'A visita de fotografia só pode ser pedida no horário de trabalho do fotógrafo.';
  }
  if (!opts?.allowLunch && calRangeOverlapsLunch(start, end, settings)) {
    const s = settings || defaultCalSettings();
    return `A visita de fotografia não pode ser pedida na pausa de almoço (${s.lunchStart}–${s.lunchEnd}).`;
  }
  return '';
}

function calPhotoRangeReason(events, start, end, settings, opts) {
  const timing = calPhotoTimingReason(start, end, settings, opts);
  if (timing) return timing;
  if (calPhotoVisitOverlaps(events, start, end, opts?.exceptId)) {
    return 'Esse horário já tem uma visita de fotografia.';
  }
  if (!calRangeFullyLivre(events, start, end, settings)) {
    return 'A visita de fotografia só pode ser pedida no horário livre do fotógrafo.';
  }
  return '';
}

function calDayHasLivre(events, day, settings) {
  const win = calWorkWindowOn(day, settings);
  if (!win) return false;
  return calWorkHourList(settings).some(hour => calHourShowsLivre(events, day, hour, settings));
}

function calDayCanSchedulePhoto(events, day, settings) {
  return calDayHasLivre(events, day, settings);
}

function calHourShowsLivre(events, day, hour, settings) {
  const start = calAt(day, hour);
  const end = calAt(day, hour + 1);
  return calLivreSegmentsInRange(events, start, end, settings).length > 0;
}

function calHourInWorkWindow(day, hour, settings) {
  const win = calWorkWindowOn(day, settings);
  if (!win) return false;
  const start = calAt(day, hour);
  const end = calAt(day, hour + 1);
  return start >= win.start && end <= win.end;
}

function calHourShowsBlocked(events, day, hour, settings) {
  if (!calHourInWorkWindow(day, hour, settings)) return false;
  const start = calAt(day, hour);
  const end = calAt(day, hour + 1);
  return !!calBloqueadoCovering(events, start, end);
}

function calHourSlotWash(events, day, hour, settings) {
  const start = calAt(day, hour);
  const end = calAt(day, hour + 1);
  const segs = calLivreSegmentsInRange(events, start, end, settings);
  if (segs.length === 0) {
    if (calHourShowsBlocked(events, day, hour, settings)) return { className: ' is-bloqueado' };
    return { className: '' };
  }
  const span = end.getTime() - start.getTime();
  const free = segs.reduce((n, s) => n + (s.t1 - s.t0), 0);
  if (free >= span - 60 * 1000) return { className: ' is-livre' };
  return {
    className: ' is-livre-partial',
    style: { backgroundImage: calLivreGradient(segs, start.getTime(), end.getTime()) },
  };
}

function calDayHasBlocked(events, day, settings) {
  const win = calWorkWindowOn(day, settings);
  if (!win) return false;
  return calWorkHourList(settings).some(hour => calHourShowsBlocked(events, day, hour, settings));
}

function calTodayInput() {
  return calDateInput(calStartOfDay(new Date()));
}

function calVisibleWeekRange(day, settings) {
  const start = calStartOfWeek(day);
  const end = new Date(start);
  end.setDate(end.getDate() + ((settings || defaultCalSettings()).weekendsEnabled ? 6 : 4));
  return { start, end };
}

function calManageableWeekRange(day, settings) {
  const today = calStartOfDay(new Date());
  let range = calVisibleWeekRange(day, settings);
  if (calStartOfDay(range.end) < today) range = calVisibleWeekRange(today, settings);
  const start = calStartOfDay(range.start) < today ? today : range.start;
  const end = calStartOfDay(range.end) < today ? today : range.end;
  return { start, end };
}

function calIntervalHasEnded(ev) {
  if (!ev?.end) return false;
  return calDateInput(ev.end) < calTodayInput();
}

function calIntervalCaption(ev) {
  if (!ev?.start || !ev?.end) return '';
  const same = calSameDay(ev.start, ev.end);
  const days = same
    ? ev.start.toLocaleDateString('pt-PT', { weekday: 'short', day: 'numeric', month: 'short' })
    : `${ev.start.toLocaleDateString('pt-PT', { day: 'numeric', month: 'short' })} – ${ev.end.toLocaleDateString('pt-PT', { day: 'numeric', month: 'short' })}`;
  return `${days} · ${calHm(ev.start)}–${calHm(ev.end)}`;
}

function calPhotoLeadHint(settings) {
  const n = Number(settings?.photoMinHoursAhead);
  const hours = Number.isFinite(n) && n >= 0 ? n : 2;
  if (hours === 0) return 'no futuro';
  if (hours === 1) return 'com 1 hora de antecedência';
  return `com ${hours} horas de antecedência`;
}

function isCalPhotoVisit(ev) {
  if (!ev) return false;
  const type = String(ev.type || '').toLowerCase();
  const status = String(ev.status || '').toLowerCase();
  if (status === 'livre' || status === 'bloqueado') return false;
  return type === 'fotografia' || ev.calendar === 'fotografia' || ['pendente', 'reagendar', 'aprovado'].includes(status) && (type === 'fotografia' || ev.kind === 'photo');
}

function draftFromEvent(ev) {
  const photo = isCalPhotoVisit(ev);
  return {
    id: ev.id,
    kind: photo ? 'photo' : isCalSlotType(ev.type) ? 'slot' : 'event',
    title: ev.title,
    date: calDateInput(ev.start),
    endDate: calDateInput(ev.end),
    startTime: calTimeInput(ev.start),
    endTime: calTimeInput(ev.end),
    location: ev.location || '',
    imovel: ev.imovel || ev.location || '',
    calendar: ev.calendar || (photo ? 'fotografia' : 'pessoal'),
    category: ev.category || 'trabalho',
    people: ev.people ? [...ev.people] : [],
    sensitive: !!ev.sensitive,
    slotSpan: calDateInput(ev.end) > calDateInput(ev.start),
    proposing: false,
    cancelling: false,
    cancelReason: '',
    isSystem: !!ev.isSystem,
    locked: isCalBloqueadoSlot(ev),
  };
}

function CalendarEventCard({ ev, compact, spanPos = '', onOpen }) {
  const tone = calEventTone(ev);
  const icon = calEventIcon(ev);
  const span = isCalSpanEvent(ev);
  const isBar = spanPos === 'bar';
  const tip = span
    ? [ev.title, `${ev.start.toLocaleDateString('pt-PT')} – ${ev.end.toLocaleDateString('pt-PT')}`, ev.location].filter(Boolean).join(' · ')
    : [ev.title, `${calHm(ev.start)} – ${calHm(ev.end)}`, ev.location].filter(Boolean).join(' · ');
  const spanClass = isBar
    ? ' is-span is-span-bar'
    : (spanPos ? ` is-span is-span-${spanPos}` : '');
  return (
    <AppTooltip label={tip}>
      <button
        type="button"
        className={`cal-event is-${tone}${compact ? ' is-compact' : ''}${ev.sample ? ' is-sample' : ''}${spanClass}`}
        onClick={(e) => { e.stopPropagation(); onOpen(ev); }}
      >
        <span className="cal-event-head">
          {spanPos !== 'mid' && <span className="cal-event-ico"><Icon name={icon} size={compact ? 9 : 11} /></span>}
          <span className="cal-event-title">{ev.title}</span>
        </span>
        {!compact && !span && <span className="cal-event-time">{calHm(ev.start)} – {calHm(ev.end)}</span>}
      </button>
    </AppTooltip>
  );
}

function CalendarExportActions({ ev, onError }) {
  if (!ev?.id || !ev.start || !ev.end) return null;
  const openExternal = (url) => window.open(url, '_blank', 'noopener,noreferrer');
  return (
    <div className="cal-export">
      <button type="button" className="btn btn-secondary btn-sm" onClick={() => openExternal(calGoogleUrl(ev))}>
        Google Calendar
      </button>
      <button type="button" className="btn btn-secondary btn-sm" onClick={() => openExternal(calOutlookUrl(ev))}>
        Outlook
      </button>
      <button
        type="button"
        className="btn btn-secondary btn-sm"
        onClick={async () => {
          try { await downloadCalendarIcs(ev.id); }
          catch (err) { onError?.(err.message || 'Não foi possível descarregar o evento.'); }
        }}
      >
        Descarregar .ics
      </button>
    </div>
  );
}

async function searchHabitaColaboradores(term) {
  const normalizeHits = (list) => list
    .map(h => ({
      key: String(h.key || h.id || h.Id || h.lookupId || h.LookupId || h.email || h.Email || h.assinatura || h.Assinatura || ''),
      displayName: String(h.displayName || h.DisplayName || h.assinatura || h.Assinatura || '').trim(),
      email: String(h.email || h.Email || '').trim(),
      local: String(h.local || h.Local || '').trim(),
    }))
    .filter(h => h.displayName)
    .slice(0, 20);

  const fromColaboradores = async () => {
    const params = new URLSearchParams();
    if (term) params.set('search', term);
    const result = await apiFetch(`/api/equipa/colaboradores?${params}`);
    return normalizeHits(result.items || result.Items || []);
  };
  const fromConsultores = async () => {
    const params = new URLSearchParams({ take: '20' });
    if (term) params.set('search', term);
    const result = await apiFetch(`/api/equipa/consultores?${params}`);
    const list = Array.isArray(result) ? result : (result.items || result.Items || []);
    return normalizeHits(list);
  };

  try {
    return await fromColaboradores();
  } catch (err) {
    if (err?.status === 403 || err?.status === 404) return fromConsultores();
    throw err;
  }
}

function CalendarPeopleField({ people = [], onChange, disabled }) {
  const [pickOpen, setPickOpen] = useState(false);
  const [query, setQuery] = useState('');
  const [open, setOpen] = useState(false);
  const [loading, setLoading] = useState(false);
  const [hits, setHits] = useState([]);
  const [errorMsg, setErrorMsg] = useState('');
  const [menuPos, setMenuPos] = useState(null);
  const wrapRef = useRef(null);
  const inputRef = useRef(null);
  const timerRef = useRef(null);
  const reqIdRef = useRef(0);
  const peopleRef = useRef(people);
  const queryRef = useRef('');
  peopleRef.current = people;
  queryRef.current = query;
  const isSelected = (name) => peopleRef.current.some(n => String(n).trim().toLowerCase() === String(name || '').trim().toLowerCase());

  useEffect(() => {
    if (!pickOpen) return undefined;
    const t = window.setTimeout(() => inputRef.current?.focus(), 0);
    return () => window.clearTimeout(t);
  }, [pickOpen]);

  useEffect(() => {
    const onDoc = (e) => {
      if (wrapRef.current?.contains(e.target) || e.target.closest?.('.consultor-search-menu')) return;
      setOpen(false);
    };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, []);

  const syncMenuPos = useCallback(() => {
    const el = inputRef.current;
    if (!el) return;
    const r = el.getBoundingClientRect();
    const spaceBelow = window.innerHeight - r.bottom - 8;
    const spaceAbove = r.top - 8;
    const placeAbove = spaceBelow < 160 && spaceAbove > spaceBelow;
    const maxHeight = Math.max(120, Math.min(240, placeAbove ? spaceAbove : spaceBelow));
    setMenuPos({
      top: placeAbove ? Math.max(8, r.top - maxHeight - 4) : r.bottom + 4,
      left: r.left,
      width: Math.max(r.width, 260),
      maxHeight,
    });
  }, []);

  useLayoutEffect(() => {
    if (!open || !pickOpen) return undefined;
    syncMenuPos();
    window.addEventListener('resize', syncMenuPos);
    window.addEventListener('scroll', syncMenuPos, true);
    return () => {
      window.removeEventListener('resize', syncMenuPos);
      window.removeEventListener('scroll', syncMenuPos, true);
    };
  }, [open, pickOpen, hits, loading, syncMenuPos]);

  const runSearch = (text) => {
    if (timerRef.current) clearTimeout(timerRef.current);
    const reqId = ++reqIdRef.current;
    timerRef.current = setTimeout(async () => {
      setLoading(true);
      setErrorMsg('');
      setOpen(true);
      try {
        const mapped = (await searchHabitaColaboradores(String(text || '').trim()))
          .filter(h => !isSelected(h.displayName));
        if (reqId !== reqIdRef.current) return;
        setHits(mapped);
        if (mapped.length === 0) {
          setErrorMsg(text ? 'Sem colaboradores encontrados. Tente nome ou email.' : 'Sem colaboradores.');
        }
      } catch (err) {
        if (reqId !== reqIdRef.current) return;
        setHits([]);
        setErrorMsg(err?.message || 'Erro ao pesquisar colaboradores.');
      } finally {
        if (reqId === reqIdRef.current) setLoading(false);
      }
    }, 180);
  };

  const addPerson = (hit) => {
    const name = hit?.displayName;
    if (!name || isSelected(name)) return;
    const next = [...peopleRef.current, name];
    peopleRef.current = next;
    onChange(next);
    setHits(prev => prev.filter(h => h.displayName.toLowerCase() !== name.toLowerCase()));
    queryRef.current = '';
    setQuery('');
    runSearch('');
    inputRef.current?.focus();
  };

  const removePerson = (name) => {
    onChange(people.filter(p => p !== name));
  };

  return (
    <div className="checkbox-group" ref={wrapRef}>
      <span className="checkbox-group-title">Pessoas</span>
      <div className="cal-people">
        {people.map(name => (
          <AppTooltip key={name} label={disabled ? name : `${name} · clicar para remover`}>
            <button
              type="button"
              className="cal-person"
              disabled={disabled}
              onClick={() => !disabled && removePerson(name)}
            >
              {userSigla(name)}
            </button>
          </AppTooltip>
        ))}
        {!disabled && (
          <button
            type="button"
            className={`cal-person cal-person-add${pickOpen ? ' is-on' : ''}`}
            onClick={() => {
              setPickOpen(v => {
                const next = !v;
                if (next) runSearch(query);
                else setOpen(false);
                return next;
              });
            }}
            aria-label="Adicionar colaborador"
            aria-expanded={pickOpen}
          >
            <Icon name="plus" size={12} />
          </button>
        )}
      </div>
      {pickOpen && !disabled && (
        <div className="consultor-search cal-people-search">
          <div className="consultor-search-input-row">
            <input
              ref={inputRef}
              type="text"
              value={query}
              placeholder="Pesquisar colaborador Habita…"
              autoComplete="off"
              onFocus={() => runSearch(queryRef.current)}
              onChange={e => {
                const text = e.target.value;
                queryRef.current = text;
                setQuery(text);
                runSearch(text);
              }}
              onKeyDown={e => {
                if (e.key === 'Escape') {
                  e.stopPropagation();
                  setOpen(false);
                  setPickOpen(false);
                  return;
                }
                if (e.key === 'Enter' && open && hits[0]) {
                  e.preventDefault();
                  addPerson(hits[0]);
                }
                if (e.key === 'ArrowDown') {
                  e.preventDefault();
                  if (!open) runSearch(query);
                }
              }}
              aria-autocomplete="list"
              aria-expanded={open}
              aria-label="Pesquisar colaborador"
            />
          </div>
          {open && menuPos && ReactDOM.createPortal(
            <div
              className="consultor-search-menu consultor-search-menu-portal"
              role="listbox"
              style={{
                position: 'fixed',
                top: menuPos.top,
                left: menuPos.left,
                width: menuPos.width,
                maxHeight: menuPos.maxHeight,
                zIndex: 13000,
              }}
            >
              {loading && <div className="consultor-search-empty">A pesquisar…</div>}
              {!loading && hits.length === 0 && (
                <div className="consultor-search-empty">{errorMsg || 'Sem resultados'}</div>
              )}
              {!loading && hits.map(h => (
                <button
                  key={h.key}
                  type="button"
                  className="consultor-search-option"
                  role="option"
                  onMouseDown={(e) => e.preventDefault()}
                  onClick={() => addPerson(h)}
                >
                  <strong>{h.displayName}</strong>
                  {(h.email || h.local) && <span>{[h.email, h.local].filter(Boolean).join(' · ')}</span>}
                </button>
              ))}
            </div>,
            document.body
          )}
        </div>
      )}
    </div>
  );
}

function CalendarEventForm({
  draft,
  setDraft,
  ev,
  error,
  saving,
  canManageSlots,
  isAdmin = false,
  calSettings,
  onClose,
  onSave,
  onDelete,
  onRequest,
  onApprove,
  onReject,
  onLock,
  onUnlock,
  onReschedule,
  onAcceptReschedule,
  onDeclineReschedule,
  onCancelVisit,
  onError,
}) {
  const people = draft.people || [];
  const creating = !draft.id;
  const status = String(ev?.status || '').toLowerCase();
  const isLivre = status === 'livre' || String(ev?.type || '').toLowerCase() === 'livre';
  const isBloqueado = status === 'bloqueado' || String(ev?.type || '').toLowerCase() === 'bloqueado';
  const isPhotoVisit = draft.kind === 'photo' || isCalPhotoVisit(ev);
  const isSlot = (draft.kind === 'slot' || isCalSlotType(ev?.type)) && !isPhotoVisit;
  const isPendente = status === 'pendente';
  const isReagendar = status === 'reagendar';
  const isAprovado = status === 'aprovado';
  const proposing = !!draft.proposing;
  const isSample = !!ev?.sample || isCalSampleId(draft.id);
  const isSystem = !!(ev?.isSystem || draft.isSystem);
  const canEdit = !isSample && (creating || !!ev?.canEdit);
  const canApprovePhoto = !!ev?.canApprovePhoto;
  const canReschedulePhoto = !!ev?.canReschedulePhoto;
  const canAcceptReschedule = !!ev?.canAcceptReschedule;
  const canCancelPhoto = !!ev?.canCancelPhoto;
  const canCancelApprovedPhoto = !!ev?.canCancelApprovedPhoto;
  const requestMode = isLivre && !canManageSlots && !creating;
  const cancelling = !!draft.cancelling;
  const readOnly = !creating && !canEdit && !requestMode && !proposing && !cancelling && !isPhotoVisit;
  const slotSpan = !!draft.slotSpan;
  const showEndDate = !isSlot || slotSpan;
  const hideDates = (creating && isPhotoVisit) || requestMode;
  const timesLocked = readOnly || requestMode || proposing === false && ((isSlot && !creating) || (isPhotoVisit && !creating));

  const dialogLabel = requestMode
    ? 'Pedir horário'
    : creating && isPhotoVisit
      ? 'Visita de fotografia'
      : creating && draft.kind === 'slot'
        ? 'Horário livre'
        : draft.id ? 'Evento' : 'Novo evento';
  const personalSources = CAL_SOURCES.filter(src => src.id !== 'fotografia');
  const setField = (key, value) => setDraft(d => ({ ...d, [key]: value }));
  const headerIcon = calEventIcon(ev || {
    type: isPhotoVisit ? 'fotografia' : draft.kind === 'slot' ? 'livre' : 'evento',
    status: isPhotoVisit ? 'pendente' : draft.kind === 'slot' ? 'livre' : 'ativo',
    calendar: draft.calendar,
    category: draft.category,
    sensitive: draft.sensitive,
  });

  return (
    <aside className="chart-filter-popover cal-event-popover" role="dialog" aria-label={dialogLabel}>
      <div className="chart-filter-popover-header cal-event-popover-header">
        <span className={`cal-event-ico is-${calEventTone(ev || { category: draft.category, type: draft.kind === 'slot' ? 'livre' : 'evento', status: draft.kind === 'slot' ? 'livre' : '' })}`}>
          <Icon name={headerIcon} size={13} />
        </span>
        {readOnly || (isSlot && !creating && !requestMode) ? (
          <strong className="cal-event-title-input">{draft.title || 'Evento'}</strong>
        ) : (
          <input
            className="cal-event-title-input"
            value={draft.title}
            onChange={e => setField('title', e.target.value)}
            placeholder={requestMode || isPhotoVisit ? 'Visita de fotografia' : draft.kind === 'slot' ? 'Horário livre' : 'Título do evento'}
            aria-label="Título"
          />
        )}
        <div className="cal-event-popover-tools">
          {isAdmin && draft.id && !creating && !isSample && (
            <AppTooltip label="Eliminar sem notificar">
              <button
                type="button"
                className="btn-icon-ghost danger"
                disabled={saving}
                onClick={onDelete}
                aria-label="Eliminar evento"
              >
                <Icon name="trash" size={14} />
              </button>
            </AppTooltip>
          )}
          <AppTooltip label="Fechar">
            <button type="button" className="btn-icon-ghost" onClick={onClose} aria-label="Fechar">
              <Icon name="close" size={14} />
            </button>
          </AppTooltip>
        </div>
      </div>
      <div className="chart-filter-popover-body">
        {isSample && <p className="cal-event-owner">Evento de exemplo — não pode ser alterado</p>}
        {isSystem && !isSample && (
          <p className="cal-event-owner">Evento do sistema — visível para toda a equipa{isAdmin ? '' : '. Só um administrador pode alterar.'}</p>
        )}
        {ev?.ownerName && !isSlot && !isSample && (
          <p className="cal-event-owner">
            {ev.sensitiveHidden ? 'Evento de outro consultor' : `Evento de ${ev.ownerName}`}
            {!canEdit && !creating && !isPhotoVisit && !isSystem ? ' — só o autor ou um administrador pode alterar.' : ''}
          </p>
        )}
        {ev?.requestedBy && (
          <p className="cal-event-owner">Pedido de {ev.requestedBy}</p>
        )}
        {isReagendar && ev?.proposedStart && (
          <p className="cal-event-owner">
            O fotógrafo propôs {formatDateTimePt(ev.proposedStart, false)}
            {ev.proposedEnd ? ` – ${calHm(ev.proposedEnd)}` : ''}.
            {' '}O consultor tem de aceitar este horário.
          </p>
        )}
        {creating && isSlot && (
          <div className="tabs tabs-segmented cal-slot-span" role="tablist" aria-label="Duração do horário livre">
            <button
              type="button"
              className={`tab${!slotSpan ? ' active' : ''}`}
              role="tab"
              aria-selected={!slotSpan}
              onClick={() => setDraft(d => ({ ...d, slotSpan: false, endDate: d.date }))}
            >
              Dia
            </button>
            <button
              type="button"
              className={`tab${slotSpan ? ' active' : ''}`}
              role="tab"
              aria-selected={slotSpan}
              onClick={() => setDraft(d => ({ ...d, slotSpan: true, endDate: d.endDate || d.date }))}
            >
              Intervalo
            </button>
          </div>
        )}
        {!hideDates && (
          <div className="chart-filter-row">
            <div className="form-group">
              <label>{showEndDate ? 'Data início' : 'Data'}</label>
              <DatePickerField
                value={draft.date}
                disabled={timesLocked}
                onChange={(date) => {
                  setDraft(d => ({
                    ...d,
                    date,
                    endDate: !d.slotSpan || !d.endDate || d.endDate < date ? date : d.endDate,
                  }));
                }}
              />
            </div>
            {showEndDate && (
              <div className="form-group">
                <label>Data fim</label>
                <DatePickerField
                  value={draft.endDate || draft.date}
                  min={draft.date}
                  disabled={timesLocked}
                  onChange={(endDate) => setField('endDate', endDate)}
                />
              </div>
            )}
          </div>
        )}
        {!hideDates && (draft.endDate || draft.date) > draft.date && (
          <p className="cal-span-hint">Vários dias aparecem na vista Mensal como um horário contínuo.</p>
        )}
        <div className="chart-filter-row">
          <div className="form-group">
            <label>Início</label>
            <TimePickerField
              value={draft.startTime}
              disabled={timesLocked}
              onChange={(startTime) => setField('startTime', startTime)}
            />
          </div>
          <div className="form-group">
            <label>Fim</label>
            <TimePickerField
              value={draft.endTime}
              disabled={timesLocked}
              onChange={(endTime) => setField('endTime', endTime)}
            />
          </div>
        </div>
        {(requestMode || isPhotoVisit) && (
          <div className="form-group">
            <label>Imóvel</label>
            <input
              value={draft.imovel}
              disabled={!creating && !proposing && !requestMode}
              onChange={e => setField('imovel', e.target.value)}
              placeholder="Referência do imóvel"
            />
          </div>
        )}
        {creating && isPhotoVisit && (
          <p className="empty-hint">
            Só no horário livre do fotógrafo, {calPhotoLeadHint(calSettings)}, e dentro do horário de trabalho
            {' '}({calWorkHoursCaption(calSettings)}).
          </p>
        )}
        {!isSlot && !isPhotoVisit && (
          <>
            <div className="form-group">
              <label>Local</label>
              <input value={draft.location} disabled={readOnly} onChange={e => setField('location', e.target.value)} placeholder="Onde" />
            </div>
            <div className="form-group">
              <label>Calendário</label>
              <AppSelect
                value={draft.calendar}
                disabled={readOnly}
                options={personalSources.map(src => ({ value: src.id, label: src.label }))}
                onChange={v => setField('calendar', v)}
                ariaLabel="Calendário"
              />
            </div>
            <CalendarPeopleField
              people={people}
              disabled={readOnly}
              onChange={next => setDraft(d => ({ ...d, people: next }))}
            />
            {canEdit && !isSystem && (
              <label className="avaliacao-check">
                <input
                  type="checkbox"
                  checked={!!draft.sensitive}
                  onChange={e => setField('sensitive', e.target.checked)}
                />
                Evento sensível
              </label>
            )}
            {isAdmin && !isPhotoVisit && !isSlot && (
              <label className="avaliacao-check">
                <input
                  type="checkbox"
                  checked={!!draft.isSystem}
                  disabled={!creating && !canEdit}
                  onChange={e => setField('isSystem', e.target.checked)}
                />
                Evento do sistema
              </label>
            )}
          </>
        )}
        {ev && !cancelling && <CalendarExportActions ev={ev} onError={onError} />}
        {cancelling && (
          <div className="form-group cal-cancel-reason">
            <label htmlFor="cal-cancel-reason">Motivo do cancelamento</label>
            <textarea
              id="cal-cancel-reason"
              rows={3}
              value={draft.cancelReason || ''}
              onChange={e => setField('cancelReason', e.target.value)}
              placeholder="Explique porque o fotógrafo não pode ir."
            />
            <small>O consultor que pediu a visita recebe este motivo.</small>
          </div>
        )}
        {error && <p className="empty-hint form-hint-error" role="alert">{error}</p>}
      </div>
      <div className="chart-filter-popover-actions">
        {requestMode && (
          <button type="button" className="btn btn-primary btn-sm" disabled={saving} onClick={onRequest}>
            Pedir horário
          </button>
        )}
        {creating && isPhotoVisit && (
          <button type="button" className="btn btn-primary btn-sm" disabled={saving} onClick={onSave}>
            Pedir visita
          </button>
        )}
        {canManageSlots && isLivre && !creating && (
          <button type="button" className="btn btn-secondary btn-sm" disabled={saving} onClick={onLock}>Bloquear</button>
        )}
        {canManageSlots && isBloqueado && (
          <button type="button" className="btn btn-primary btn-sm" disabled={saving} onClick={onUnlock}>Desbloquear</button>
        )}
        {canApprovePhoto && !proposing && !cancelling && (
          <>
            <button type="button" className="btn btn-primary btn-sm" disabled={saving} onClick={onApprove}>Aprovar</button>
            <button type="button" className="btn btn-secondary btn-sm" disabled={saving} onClick={onReject}>Recusar</button>
          </>
        )}
        {canReschedulePhoto && !proposing && !cancelling && (
          <button type="button" className="btn btn-secondary btn-sm" disabled={saving} onClick={() => setDraft(d => {
            const start = ev?.proposedStart || ev?.start;
            const end = ev?.proposedEnd || ev?.end;
            return {
              ...d,
              proposing: true,
              cancelling: false,
              date: start ? calDateInput(start) : d.date,
              endDate: end ? calDateInput(end) : d.endDate,
              startTime: start ? calTimeInput(start) : d.startTime,
              endTime: end ? calTimeInput(end) : d.endTime,
            };
          })}>Reagendar</button>
        )}
        {canReschedulePhoto && proposing && (
          <button type="button" className="btn btn-primary btn-sm" disabled={saving} onClick={onReschedule}>Propor horário</button>
        )}
        {canAcceptReschedule && !proposing && !cancelling && (
          <>
            <button type="button" className="btn btn-primary btn-sm" disabled={saving} onClick={onAcceptReschedule}>Aceitar horário</button>
            <button type="button" className="btn btn-secondary btn-sm" disabled={saving} onClick={onDeclineReschedule}>Recusar proposta</button>
          </>
        )}
        {canCancelPhoto && !cancelling && (
          <button type="button" className="btn btn-secondary btn-sm" disabled={saving} onClick={onDelete}>Cancelar pedido</button>
        )}
        {canCancelApprovedPhoto && !cancelling && (
          <button
            type="button"
            className="btn btn-secondary btn-sm"
            disabled={saving}
            onClick={() => setDraft(d => ({ ...d, cancelling: true, proposing: false, cancelReason: d.cancelReason || '' }))}
          >
            Cancelar visita
          </button>
        )}
        {cancelling && (
          <>
            <button type="button" className="btn btn-primary btn-sm" disabled={saving} onClick={onCancelVisit}>
              Confirmar cancelamento
            </button>
            <button
              type="button"
              className="btn btn-secondary btn-sm"
              disabled={saving}
              onClick={() => setDraft(d => ({ ...d, cancelling: false }))}
            >
              Voltar
            </button>
          </>
        )}
        {canManageSlots && isPendente && !isPhotoVisit && (
          <>
            <button type="button" className="btn btn-primary btn-sm" disabled={saving} onClick={onApprove}>Aprovar</button>
            <button type="button" className="btn btn-secondary btn-sm" disabled={saving} onClick={onReject}>Recusar</button>
          </>
        )}
        {canEdit && !requestMode && !isPhotoVisit && (creating || !isSlot) && (
          <button type="button" className="btn btn-primary btn-sm" disabled={saving} onClick={onSave}>
            {creating ? (draft.kind === 'slot' ? 'Adicionar slot' : 'Adicionar evento') : 'Guardar'}
          </button>
        )}
        {draft.id && !isAdmin && (canEdit || (canManageSlots && isSlot)) && !requestMode && (
          <button type="button" className="btn btn-secondary btn-sm" disabled={saving} onClick={onDelete}>Apagar</button>
        )}
        <button type="button" className="btn btn-secondary btn-sm" onClick={onClose}>Fechar</button>
      </div>
    </aside>
  );
}

function CalendarDayPeek({ day, events, onOpen, onClose }) {
  const label = day.toLocaleDateString('pt-PT', { weekday: 'long', day: 'numeric', month: 'long' });
  return (
    <aside className="chart-filter-popover cal-event-popover cal-day-peek" role="dialog" aria-label={label}>
      <div className="chart-filter-popover-header">
        <div className="chart-filter-popover-title" style={{ textTransform: 'capitalize' }}>{label}</div>
        <AppTooltip label="Fechar">
          <button type="button" className="btn-icon-ghost" onClick={onClose} aria-label="Fechar">
            <Icon name="close" size={14} />
          </button>
        </AppTooltip>
      </div>
      <div className="chart-filter-popover-body">
        <div className="cal-day-peek-list">
          {events.map(ev => (
            <CalendarEventCard
              key={ev.id}
              ev={ev}
              compact
              spanPos={calSpanPos(ev, day)}
              onOpen={onOpen}
            />
          ))}
        </div>
      </div>
    </aside>
  );
}

function CalendarSlotChoice({ onEvent, onPhoto, onSlot, onEditSlot, onClose, canManageSlots, coveringSlot, photoAllowed, photoHint }) {
  const canPhoto = !!photoAllowed;
  const hint = photoHint || (canPhoto
    ? 'Fotógrafo recebe o pedido para aprovar ou reagendar'
    : 'Só no horário livre do fotógrafo');
  const photoButton = (
    <button
      type="button"
      className={`cal-choice-option${canPhoto ? '' : ' is-disabled'}`}
      onClick={canPhoto ? onPhoto : undefined}
      disabled={!canPhoto}
      aria-disabled={!canPhoto}
    >
      <span className="cal-choice-option-ico" aria-hidden="true"><Icon name="camera" size={14} /></span>
      <span>
        <strong>Visita de fotografia</strong>
        <small>{hint}</small>
      </span>
    </button>
  );
  return (
    <aside className="chart-filter-popover cal-event-popover cal-choice-popover" role="dialog" aria-label="Adicionar">
      <div className="chart-filter-popover-header">
        <div className="chart-filter-popover-title">Adicionar</div>
        <AppTooltip label="Fechar">
          <button type="button" className="btn-icon-ghost" onClick={onClose} aria-label="Fechar">
            <Icon name="close" size={14} />
          </button>
        </AppTooltip>
      </div>
      <div className="chart-filter-popover-body">
        <p className="cal-choice-lead">O que quer colocar neste horário?</p>
        <button type="button" className="cal-choice-option" onClick={onEvent}>
          <span className="cal-choice-option-ico" aria-hidden="true"><Icon name="calendar" size={14} /></span>
          <span>
            <strong>Evento</strong>
            <small>Compromisso no seu calendário</small>
          </span>
        </button>
        {canPhoto ? photoButton : (
          <AppTooltip label={hint}>
            <div className="cal-choice-option-wrap">{photoButton}</div>
          </AppTooltip>
        )}
        {canManageSlots && coveringSlot && (
          <button type="button" className="cal-choice-option" onClick={onEditSlot}>
            <span className="cal-choice-option-ico" aria-hidden="true"><Icon name="sliders" size={14} /></span>
            <span>
              <strong>{isCalBloqueadoSlot(coveringSlot) ? 'Gerir horário bloqueado' : 'Gerir horário livre'}</strong>
              <small>Alterar ou remover este intervalo</small>
            </span>
          </button>
        )}
        {canManageSlots && (
          <button type="button" className="cal-choice-option" onClick={onSlot}>
            <span className="cal-choice-option-ico" aria-hidden="true"><Icon name="calendar" size={14} /></span>
            <span>
              <strong>Horário livre</strong>
              <small>Disponível para pedidos de fotografia</small>
            </span>
          </button>
        )}
      </div>
    </aside>
  );
}

function calHoursFromTimeRange(fromStr, toStr, hours = CAL_WEEK_HOURS) {
  const parseH = (s) => {
    if (!s) return null;
    const hh = Number(String(s).split(':')[0]);
    return Number.isFinite(hh) ? hh : null;
  };
  const from = parseH(fromStr);
  if (from == null) return [];
  const to = parseH(toStr);
  const end = to == null || to <= from ? from + 1 : to;
  return hours.filter(h => h >= from && h < end);
}

function calWorkHourList(settings) {
  const s = settings || defaultCalSettings();
  const start = calParseHmParts(s.workStart, '09:00').h;
  const end = calParseHmParts(s.workEnd, '18:00').h;
  const hours = [];
  for (let h = start; h < end; h += 1) {
    const probe = new Date(2024, 0, 1, h);
    if (calRangeOverlapsLunch(probe, new Date(2024, 0, 1, h + 1), s)) continue;
    hours.push(h);
  }
  return hours.length ? hours : CAL_WEEK_HOURS;
}

function CalendarWeekForm({
  from,
  to,
  setFrom,
  setTo,
  mode,
  setMode,
  events,
  error,
  saving,
  editingId,
  onSave,
  onEdit,
  onDelete,
  onClose,
}) {
  const locked = mode === 'bloqueado';
  const today = calTodayInput();
  const fromMin = today;
  const toMin = from && from > today ? from : today;
  const pastRange = !!(from && from < today) || !!(to && to < today);
  const intervals = (events || [])
    .filter(isCalAvailabilitySlot)
    .slice()
    .sort((a, b) => a.start - b.start || a.end - b.end);
  return (
    <aside className="chart-filter-popover cal-event-popover" role="dialog" aria-label="Gestão Semanal">
      <div className="chart-filter-popover-header">
        <div className="chart-filter-popover-title">Gestão Semanal</div>
        <AppTooltip label="Fechar">
          <button type="button" className="btn-icon-ghost" onClick={onClose} aria-label="Fechar">
            <Icon name="close" size={14} />
          </button>
        </AppTooltip>
      </div>
      <div className="chart-filter-popover-body">
        <div className="tabs tabs-segmented cal-slot-span" role="tablist" aria-label="Tipo de semana">
          <button
            type="button"
            className={`tab${!locked ? ' active' : ''}`}
            role="tab"
            aria-selected={!locked}
            onClick={() => setMode('livre')}
          >
            Livre
          </button>
          <button
            type="button"
            className={`tab${locked ? ' active' : ''}`}
            role="tab"
            aria-selected={locked}
            onClick={() => setMode('bloqueado')}
          >
            Bloqueada
          </button>
        </div>
        <div className="chart-filter-row">
          <div className="form-group">
            <label>Início</label>
            <DatePickerField
              value={from}
              min={fromMin}
              onChange={(next) => {
                const start = next && next < today ? today : next;
                setFrom(start);
                if (to && start && start > to) setTo(start);
              }}
            />
          </div>
          <div className="form-group">
            <label>Fim</label>
            <DatePickerField
              value={to}
              min={toMin}
              onChange={(next) => {
                const floor = toMin;
                setTo(next && next < floor ? floor : next);
              }}
            />
          </div>
        </div>
        {pastRange && (
          <p className="empty-hint">Não é possível marcar ou bloquear uma semana já passada.</p>
        )}
        {error && <p className="empty-hint form-hint-error" role="alert">{error}</p>}
        <div className="cal-week-manage">
          <span className="checkbox-group-title">Gerir horários</span>
          {intervals.length === 0 && (
            <p className="cal-span-hint">Ainda não há intervalos livres ou bloqueados.</p>
          )}
          {intervals.map(ev => {
            const blocked = isCalBloqueadoSlot(ev);
            const ended = calIntervalHasEnded(ev);
            return (
              <div key={ev.id} className={`cal-week-manage-item${editingId === ev.id ? ' is-editing' : ''}`}>
                <i className={`cal-legend-swatch ${blocked ? 'is-bloqueado' : 'is-livre'}`} aria-hidden="true" />
                <span>
                  <strong>{blocked ? 'Bloqueado' : 'Livre'}</strong>
                  <small>{calIntervalCaption(ev)}</small>
                </span>
                <span className="cal-week-manage-actions">
                  <AppTooltip label={ended ? 'Intervalo já passou' : 'Editar'}>
                    <button
                      type="button"
                      className="btn-icon-ghost"
                      aria-label={ended ? 'Intervalo já passou' : 'Editar'}
                      disabled={ended}
                      onClick={() => { if (!ended) onEdit(ev); }}
                    >
                      <Icon name="edit" size={13} />
                    </button>
                  </AppTooltip>
                  <AppTooltip label="Remover">
                    <button
                      type="button"
                      className="btn-icon-ghost"
                      aria-label="Remover"
                      disabled={saving}
                      onClick={() => onDelete(ev)}
                    >
                      <Icon name="trash" size={13} />
                    </button>
                  </AppTooltip>
                </span>
              </div>
            );
          })}
        </div>
      </div>
      <div className="chart-filter-popover-actions">
        <button
          type="button"
          className="btn btn-primary btn-sm"
          disabled={saving || !from || !to || to < from || pastRange}
          onClick={onSave}
        >
          {editingId ? 'Guardar alterações' : locked ? 'Bloquear semana' : 'Marcar semana livre'}
        </button>
        <button type="button" className="btn btn-secondary btn-sm" onClick={onClose}>Cancelar</button>
      </div>
    </aside>
  );
}

function CalendarWorkHoursForm({ settings, error, saving, onSave, onClose }) {
  const initial = settings || defaultCalSettings();
  const [workStart, setWorkStart] = useState(initial.workStart);
  const [workEnd, setWorkEnd] = useState(initial.workEnd);
  const [weekendsEnabled, setWeekendsEnabled] = useState(!!initial.weekendsEnabled);
  const [lunchEnabled, setLunchEnabled] = useState(initial.lunchEnabled !== false);
  const [lunchStart, setLunchStart] = useState(initial.lunchStart || '13:00');
  const [lunchEnd, setLunchEnd] = useState(initial.lunchEnd || '14:00');
  return (
    <aside className="chart-filter-popover cal-event-popover" role="dialog" aria-label="Horário de trabalho">
      <div className="chart-filter-popover-header">
        <div className="chart-filter-popover-title">Horário de trabalho</div>
        <AppTooltip label="Fechar">
          <button type="button" className="btn-icon-ghost" onClick={onClose} aria-label="Fechar">
            <Icon name="close" size={14} />
          </button>
        </AppTooltip>
      </div>
      <div className="chart-filter-popover-body">
        <p className="empty-hint">
          O horário livre só aparece neste intervalo. Pedidos de fotografia fora destas horas não são aceites.
          {lunchEnabled ? ' A pausa de almoço fica fechada por predefinição — pode alterar o intervalo ou desmarcar se precisar de agendar nessa hora.' : ''}
        </p>
        <div className="chart-filter-row">
          <div className="form-group">
            <label>Início</label>
            <TimePickerField value={workStart} onChange={setWorkStart} />
          </div>
          <div className="form-group">
            <label>Fim</label>
            <TimePickerField value={workEnd} min={workStart || undefined} onChange={setWorkEnd} />
          </div>
        </div>
        <label className="avaliacao-check">
          <input
            type="checkbox"
            checked={lunchEnabled}
            onChange={e => setLunchEnabled(e.target.checked)}
          />
          Pausa de almoço
        </label>
        {lunchEnabled && (
          <div className="chart-filter-row">
            <div className="form-group">
              <label>Almoço início</label>
              <TimePickerField value={lunchStart} min={workStart || undefined} max={workEnd || undefined} onChange={setLunchStart} />
            </div>
            <div className="form-group">
              <label>Almoço fim</label>
              <TimePickerField value={lunchEnd} min={lunchStart || workStart || undefined} max={workEnd || undefined} onChange={setLunchEnd} />
            </div>
          </div>
        )}
        <label className="avaliacao-check">
          <input
            type="checkbox"
            checked={weekendsEnabled}
            onChange={e => setWeekendsEnabled(e.target.checked)}
          />
          Disponível ao fim de semana
        </label>
        {error && <p className="empty-hint form-hint-error" role="alert">{error}</p>}
      </div>
      <div className="chart-filter-popover-actions">
        <button
          type="button"
          className="btn btn-primary btn-sm"
          disabled={saving}
          onClick={() => onSave({ workStart, workEnd, weekendsEnabled, lunchEnabled, lunchStart, lunchEnd })}
        >
          Guardar horário
        </button>
        <button type="button" className="btn btn-secondary btn-sm" onClick={onClose}>Cancelar</button>
      </div>
    </aside>
  );
}

function calNotifEventId(n) {
  const raw = String(n?.id || n?.Id || '');
  const match = raw.match(/^foto-(?:pendente|reagendar|aprovado)-(.+)$/i);
  return match ? match[1] : '';
}

function isCalNotif(n) {
  const tipo = n?.tipo || n?.Tipo || '';
  return /fotografia|calendar/i.test(tipo);
}

function isCalActionNotif(n) {
  const id = String(n?.id || n?.Id || '');
  if (/^foto-cancelada-/i.test(id)) return false;
  const requires = Boolean(n?.requiresAction ?? n?.RequiresAction);
  return requires && isCalNotif(n);
}

function CalendarPage({ user, onNavigate }) {
  const today = useMemo(() => calStartOfDay(new Date()), []);
  const { isPhone, isTablet } = useViewportNav();
  const [view, setView] = useState('semana');
  const [cursor, setCursor] = useState(() => calStartOfDay(new Date()));
  const [miniMonth, setMiniMonth] = useState(() => calStartOfDay(new Date()));
  const [events, setEvents] = useState([]);
  const [canManageSlots, setCanManageSlots] = useState(false);
  const [calSettings, setCalSettings] = useState(defaultCalSettings);
  const [hoursOpen, setHoursOpen] = useState(false);
  const [showNotifications, setShowNotifications] = useState(false);
  const [notifItems, setNotifItems] = useState([]);
  const [notifLoading, setNotifLoading] = useState(false);
  const [notifError, setNotifError] = useState(null);
  const [notifTick, setNotifTick] = useState(0);
  const [pendingOpenId, setPendingOpenId] = useState(() => {
    try { return sessionStorage.getItem('myhabita.cal-open-event') || ''; } catch { return ''; }
  });
  const [loading, setLoading] = useState(true);
  const [loadError, setLoadError] = useState(null);
  const [saving, setSaving] = useState(false);
  const [sources, setSources] = useState(() => CAL_SOURCES.map(s => s.id));
  const [categories, setCategories] = useState(() => CAL_CATEGORIES.map(c => c.id));
  const [draft, setDraft] = useState(null);
  const [choice, setChoice] = useState(null);
  const [weekOpen, setWeekOpen] = useState(false);
  const [weekMode, setWeekMode] = useState('livre');
  const [weekFrom, setWeekFrom] = useState('');
  const [weekTo, setWeekTo] = useState('');
  const [weekEditingId, setWeekEditingId] = useState('');
  const [formError, setFormError] = useState(null);
  const showCalFormError = (msg) => {
    const text = String(msg || 'Não foi possível guardar.');
    setFormError(text);
    toast.error(text);
  };
  const [hiddenSamples, setHiddenSamples] = useState(() => new Set());
  const [dayPeek, setDayPeek] = useState(null);
  const [asideCollapsed, setAsideCollapsed] = useState(() => {
    try {
      const stored = localStorage.getItem('myhabita.cal-aside-collapsed');
      if (stored === '0' || stored === '1') return stored === '1';
    } catch { /* ignore */ }
    return typeof window !== 'undefined' && window.innerWidth <= BP_TABLET_MAX;
  });
  const popoverRef = useRef(null);

  const persistAsideCollapsed = (next) => {
    setAsideCollapsed(next);
    try { localStorage.setItem('myhabita.cal-aside-collapsed', next ? '1' : '0'); } catch { /* ignore */ }
  };

  const toggleAside = () => {
    setAsideCollapsed(v => {
      const next = !v;
      try { localStorage.setItem('myhabita.cal-aside-collapsed', next ? '1' : '0'); } catch { /* ignore */ }
      return next;
    });
  };

  const applyDoc = (doc) => {
    const mapped = mapCalendarDoc(doc);
    setEvents(mapped.events);
    setCanManageSlots(mapped.canManageSlots || userIsAdmin(user));
    setCalSettings(mapped.settings || defaultCalSettings());
    setNotifTick(t => t + 1);
  };

  const closeOverlays = () => {
    setDraft(null);
    setChoice(null);
    setWeekOpen(false);
    setWeekEditingId('');
    setHoursOpen(false);
    setDayPeek(null);
    setFormError(null);
  };

  useEffect(() => {
    let cancelled = false;
    (async () => {
      setLoading(true);
      setLoadError(null);
      try {
        const doc = await apiFetch('/api/calendar');
        if (!cancelled) applyDoc(doc);
      } catch (err) {
        if (!cancelled) {
          setEvents([]);
          setLoadError(err.message || 'Não foi possível carregar o calendário.');
        }
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, [user?.email, user?.Email]);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      setNotifLoading(true);
      setNotifError(null);
      try {
        const data = await apiFetch('/api/notificacoes/me');
        if (!cancelled) setNotifItems(data.items || data.Items || []);
      } catch (err) {
        if (!cancelled) {
          setNotifItems([]);
          setNotifError(err.message || 'Não foi possível carregar notificações.');
        }
      } finally {
        if (!cancelled) setNotifLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, [user?.email, user?.Email, notifTick]);

  const overlayOpen = !!(draft || choice || weekOpen || hoursOpen || dayPeek);
  const floatFilters = isPhone || isTablet;
  useEffect(() => {
    if (asideCollapsed || !floatFilters) return undefined;
    const onKey = (e) => {
      if (e.key === 'Escape') persistAsideCollapsed(true);
    };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [asideCollapsed, floatFilters]);
  useEffect(() => {
    if (!overlayOpen) return undefined;
    const onDoc = (e) => {
      if (popoverRef.current?.contains(e.target)) return;
      if (e.target.closest?.('.consultor-search-menu')) return;
      if (e.target.closest?.('.date-picker-panel')) return;
      if (e.target.closest?.('.time-picker-panel')) return;
      closeOverlays();
    };
    const onKey = (e) => {
      if (e.key === 'Escape') closeOverlays();
    };
    const timer = window.setTimeout(() => {
      document.addEventListener('mousedown', onDoc);
    }, 0);
    document.addEventListener('keydown', onKey);
    return () => {
      window.clearTimeout(timer);
      document.removeEventListener('mousedown', onDoc);
      document.removeEventListener('keydown', onKey);
    };
  }, [overlayOpen]);

  const sampleEvents = useMemo(
    () => (events.length ? [] : buildSampleCalendarEvents(today)),
    [today, events.length]
  );
  const mergedEvents = useMemo(() => mergeCalendarEvents(events, sampleEvents), [events, sampleEvents]);
  const calNotifs = useMemo(
    () => withLatestReleaseNotification((notifItems || []).filter(isCalNotif)),
    [notifItems]
  );
  const visible = mergedEvents.filter(ev => sources.includes(ev.calendar) && categories.includes(ev.category));

  useEffect(() => {
    if (!pendingOpenId || loading) return;
    const ev = mergedEvents.find(e => String(e.id) === String(pendingOpenId));
    if (!ev) {
      setPendingOpenId('');
      try { sessionStorage.removeItem('myhabita.cal-open-event'); } catch { /* ignore */ }
      return;
    }
    try { sessionStorage.removeItem('myhabita.cal-open-event'); } catch { /* ignore */ }
    setPendingOpenId('');
    setChoice(null);
    setWeekOpen(false);
    setDayPeek(null);
    setFormError(null);
    if (ev.start instanceof Date && !Number.isNaN(ev.start.getTime())) {
      setCursor(calStartOfDay(ev.start));
      setMiniMonth(new Date(ev.start.getFullYear(), ev.start.getMonth(), 1));
    }
    setDraft(draftFromEvent(ev));
  }, [pendingOpenId, loading, mergedEvents]);
  const weekStart = calStartOfWeek(cursor);
  const days = view === 'dia'
    ? [new Date(cursor)]
    : Array.from({ length: 7 }, (_, i) => {
        const d = new Date(weekStart);
        d.setDate(d.getDate() + i);
        return d;
      });
  const monthLabel = `${cursor.toLocaleDateString('pt-PT', { month: 'long' })} ${cursor.getFullYear()}`;
  const nextEvent = visible
    .filter(ev => ev.end >= new Date() && !isCalAvailabilitySlot(ev))
    .sort((a, b) => a.start - b.start)[0] || null;

  const eventsOn = (day) => visible
    .filter(ev => calEventCoversDay(ev, day))
    .sort((a, b) => {
      const spanDiff = Number(isCalSpanEvent(b)) - Number(isCalSpanEvent(a));
      if (spanDiff) return spanDiff;
      return a.start - b.start;
    });
  const displayEventsOn = (day) => eventsOn(day).filter(ev => !isCalAvailabilitySlot(ev));

  const monthCells = useMemo(() => {
    const first = new Date(cursor.getFullYear(), cursor.getMonth(), 1);
    const start = calStartOfWeek(first);
    return Array.from({ length: 42 }, (_, i) => {
      const d = new Date(start);
      d.setDate(start.getDate() + i);
      return d;
    });
  }, [cursor]);

  const miniCells = useMemo(() => {
    const first = new Date(miniMonth.getFullYear(), miniMonth.getMonth(), 1);
    const start = calStartOfWeek(first);
    return Array.from({ length: 42 }, (_, i) => {
      const d = new Date(start);
      d.setDate(start.getDate() + i);
      return d;
    });
  }, [miniMonth]);

  const shift = (dir) => {
    const next = new Date(cursor);
    if (view === 'dia') next.setDate(next.getDate() + dir);
    else if (view === 'mes') next.setMonth(next.getMonth() + dir);
    else next.setDate(next.getDate() + dir * 7);
    setCursor(next);
    setMiniMonth(new Date(next.getFullYear(), next.getMonth(), 1));
  };

  const goToday = () => {
    const now = calStartOfDay(new Date());
    setCursor(now);
    setMiniMonth(new Date(now.getFullYear(), now.getMonth(), 1));
  };

  const toggleList = (list, id, setList) => {
    setList(list.includes(id) ? (list.length === 1 ? list : list.filter(x => x !== id)) : [...list, id]);
  };

          const openNew = (day, hour, kind = 'event') => {
    setChoice(null);
    setWeekOpen(false);
    setFormError(null);
    let next = emptyCalDraft(day || cursor, hour ?? 9, kind);
    if (kind === 'photo') {
      const from = calAt(day || cursor, hour ?? 9);
      const to = calAt(day || cursor, (hour ?? 9) + 1);
      const segs = calLivreSegmentsInRange(mergedEvents, from, to, calSettings);
      if (segs[0]) {
        const s = new Date(segs[0].t0);
        const e = new Date(Math.min(segs[0].t1, segs[0].t0 + 60 * 60 * 1000));
        next = {
          ...next,
          date: calDateInput(s),
          endDate: calDateInput(s),
          startTime: calTimeInput(s),
          endTime: calTimeInput(e),
        };
      }
    }
    setDraft(next);
  };

  const openEvent = (ev) => {
    setChoice(null);
    setWeekOpen(false);
    setDayPeek(null);
    setFormError(null);
    if (ev?.start instanceof Date && !Number.isNaN(ev.start.getTime())) {
      setCursor(calStartOfDay(ev.start));
      setMiniMonth(new Date(ev.start.getFullYear(), ev.start.getMonth(), 1));
    }
    setDraft(draftFromEvent(ev));
  };

  const openFromNotif = (n) => {
    setShowNotifications(false);
    const eventId = calNotifEventId(n);
    if (!eventId) return;
    const ev = mergedEvents.find(e => String(e.id) === String(eventId));
    if (ev) openEvent(ev);
  };

  const openEmpty = (day, hour) => {
    setDraft(null);
    setWeekOpen(false);
    setFormError(null);
    setChoice({ day, hour });
  };

  const runCal = async (fn, keepOpen = false) => {
    setSaving(true);
    setFormError(null);
    try {
      const doc = await fn();
      applyDoc(doc);
      if (!keepOpen) closeOverlays();
    } catch (err) {
      showCalFormError(err.message || 'Não foi possível guardar.');
    } finally {
      setSaving(false);
    }
  };

  const saveDraft = () => {
    if (!draft) return;
    const start = calParseDateTime(draft.date, draft.startTime);
    const end = calParseDateTime(draft.endDate || draft.date, draft.endTime);
    if (!(end > start)) {
      showCalFormError('A data e hora de fim têm de ser depois do início.');
      return;
    }
    if (draft.kind === 'photo' && !draft.id) {
      const photoError = calPhotoRangeReason(mergedEvents, start, end, calSettings);
      if (photoError) {
        showCalFormError(photoError);
        return;
      }
      const title = String(draft.title || '').trim() || 'Visita de fotografia';
      runCal(() => apiFetch('/api/calendar/photo-visits', {
        method: 'POST',
        body: JSON.stringify({
          title,
          start: calApiDate(start),
          end: calApiDate(end),
          imovel: String(draft.imovel || '').trim(),
          location: String(draft.location || draft.imovel || '').trim(),
        }),
      }));
      return;
    }
    if (draft.kind === 'slot') {
      if (calStartOfDay(end).getTime() > calStartOfDay(start).getTime()) setView('mes');
      const payload = {
        start: calApiDate(start),
        end: calApiDate(end),
        locked: !!draft.locked,
      };
      if (!draft.id) {
        runCal(() => apiFetch('/api/calendar/slots', {
          method: 'POST',
          body: JSON.stringify(payload),
        }));
        return;
      }
      runCal(() => apiFetch(`/api/calendar/slots/${encodeURIComponent(draft.id)}`, {
        method: 'PUT',
        body: JSON.stringify(payload),
      }));
      return;
    }
    const title = String(draft.title || '').trim();
    if (!title) {
      showCalFormError('Indique um título.');
      return;
    }
    const payload = {
      title,
      start: calApiDate(start),
      end: calApiDate(end),
      location: String(draft.location || '').trim(),
      calendar: draft.calendar,
      category: draft.category,
      people: draft.people || [],
      sensitive: !!draft.sensitive,
      isSystem: !!draft.isSystem,
    };
    if (calStartOfDay(end).getTime() > calStartOfDay(start).getTime()) {
      setView('mes');
    }
    if (draft.id && !isCalSampleId(draft.id)) {
      runCal(() => apiFetch(`/api/calendar/events/${encodeURIComponent(draft.id)}`, {
        method: 'PUT',
        body: JSON.stringify(payload),
      }));
      return;
    }
    if (isCalSampleId(draft.id)) {
      setHiddenSamples(prev => new Set(prev).add(draft.id));
    }
    runCal(() => apiFetch('/api/calendar/events', {
      method: 'POST',
      body: JSON.stringify(payload),
    }));
  };

  const deleteDraft = () => {
    if (!draft?.id) return;
    if (isCalSampleId(draft.id)) {
      setHiddenSamples(prev => new Set(prev).add(draft.id));
      closeOverlays();
      return;
    }
    runCal(() => apiFetch(`/api/calendar/events/${encodeURIComponent(draft.id)}`, { method: 'DELETE' }));
  };

  const requestSlot = () => {
    if (!draft?.id) return;
    runCal(() => apiFetch('/api/calendar/requests', {
      method: 'POST',
      body: JSON.stringify({
        slotId: draft.id,
        title: String(draft.title || '').trim() || 'Pedido de fotografia',
        imovel: String(draft.imovel || '').trim(),
      }),
    }));
  };

  const slotAction = (path) => {
    if (!draft?.id) return;
    runCal(() => apiFetch(path, { method: 'POST', body: JSON.stringify({}) }));
  };

  const saveWeek = () => {
    if (!weekFrom || !weekTo || weekTo < weekFrom) {
      showCalFormError('Indique a data de início e de fim.');
      return;
    }
    const today = calTodayInput();
    if (weekFrom < today || weekTo < today) {
      showCalFormError('Não é possível marcar ou bloquear uma semana já passada.');
      return;
    }
    runCal(() => apiFetch('/api/calendar/weeks', {
      method: 'POST',
      body: JSON.stringify({
        start: `${weekFrom}T00:00:00`,
        end: `${weekTo}T00:00:00`,
        locked: weekMode === 'bloqueado',
        replaceId: weekEditingId || null,
      }),
    }));
  };

  const editWeekInterval = (ev) => {
    if (calIntervalHasEnded(ev)) {
      showCalFormError('Não é possível alterar um intervalo já passado.');
      return;
    }
    setFormError(null);
    setWeekEditingId(ev.id);
    setWeekMode(isCalBloqueadoSlot(ev) ? 'bloqueado' : 'livre');
    const today = calTodayInput();
    if (ev.start instanceof Date && !Number.isNaN(ev.start.getTime())) {
      const start = calDateInput(ev.start);
      setWeekFrom(start < today ? today : start);
      setCursor(calStartOfDay(ev.start));
      setMiniMonth(new Date(ev.start.getFullYear(), ev.start.getMonth(), 1));
    }
    if (ev.end instanceof Date && !Number.isNaN(ev.end.getTime())) {
      const end = calDateInput(ev.end);
      setWeekTo(end < today ? today : end);
    }
  };

  const deleteWeekInterval = (ev) => {
    if (!ev?.id) return;
    if (weekEditingId === ev.id) setWeekEditingId('');
    runCal(() => apiFetch(`/api/calendar/events/${encodeURIComponent(ev.id)}`, { method: 'DELETE' }), true);
  };

  const saveWorkHours = (nextHours) => {
    runCal(() => apiFetch('/api/calendar/settings', {
      method: 'PUT',
      body: JSON.stringify({
        ...calSettings,
        ...nextHours,
      }),
    }));
  };

  const draftEvent = draft?.id ? mergedEvents.find(e => e.id === draft.id) || null : null;

  const miniLabel = miniMonth.toLocaleDateString('pt-PT', { month: 'long', year: 'numeric' });

  return (
    <>
      <ModuleTopbar
        icon="calendar"
        title="Calendário"
        user={user}
        onOpenNotifications={() => setShowNotifications(v => {
          const next = !v;
          if (next) setNotifTick(t => t + 1);
          return next;
        })}
        notificationsOpen={showNotifications}
        onCloseNotifications={() => setShowNotifications(false)}
        notificationCount={calNotifs.filter(n => n.requiresAction ?? n.RequiresAction).length}
        notificationsPanel={(
          <NotificationsPanel
            items={calNotifs}
            loading={notifLoading && !calNotifs.length}
            error={calNotifs.length ? null : notifError}
            calendarActionLabel="Ver no calendário"
            onOpenCalendar={openFromNotif}
            onOpenRelease={() => {
              setShowNotifications(false);
              window.dispatchEvent(new CustomEvent('myhabita:open-release-notes'));
            }}
          />
        )}
      />
      <div className="page-body">
        <div className="page-content module-layout">
          <div className={`cal-page${asideCollapsed ? ' is-aside-collapsed' : ''}${isPhone ? ' is-phone' : ''}${isTablet ? ' is-tablet' : ''}${floatFilters ? ' is-float-filters' : ''}`}>
            <button
              type="button"
              className="cal-filters-backdrop"
              aria-label="Fechar filtros"
              aria-hidden={asideCollapsed || !floatFilters}
              tabIndex={asideCollapsed || !floatFilters ? -1 : 0}
              onClick={() => persistAsideCollapsed(true)}
            />
            <div className={`cal-aside-wrap${asideCollapsed ? ' is-collapsed' : ''}`}>
            <nav className="cal-aside-rail" aria-label="Filtros do calendário">
              <AppTooltip label="Mostrar painel">
                <button
                  type="button"
                  className="cal-aside-toggle"
                  onClick={toggleAside}
                  aria-expanded={false}
                  aria-label="Mostrar painel"
                >
                  <Icon name="chevronRight" size={12} />
                </button>
              </AppTooltip>
              <AppTooltip label={`${cursor.toLocaleDateString('pt-PT', { weekday: 'long', day: 'numeric', month: 'long' })}${displayEventsOn(cursor).length ? ` · ${displayEventsOn(cursor).length} evento${displayEventsOn(cursor).length === 1 ? '' : 's'}` : ''}`}>
                <button type="button" className="cal-rail-day" onClick={toggleAside} aria-label="Abrir painel do calendário">
                  {cursor.getDate()}
                  {displayEventsOn(cursor).length > 0 && <span className="cal-rail-count">{displayEventsOn(cursor).length}</span>}
                </button>
              </AppTooltip>
              <div className="cal-rail-sep" />
              {CAL_SOURCES.map(src => {
                const on = sources.includes(src.id);
                return (
                  <AppTooltip key={src.id} label={on ? src.label : `${src.label} · oculto`}>
                    <button
                      type="button"
                      className={`cal-rail-ico is-${src.id}${on ? ' is-on' : ''}`}
                      aria-pressed={on}
                      aria-label={src.label}
                      onClick={() => toggleList(sources, src.id, setSources)}
                    >
                      <Icon name={src.icon} size={13} />
                    </button>
                  </AppTooltip>
                );
              })}
              <div className="cal-rail-sep" />
              {CAL_CATEGORIES.map(cat => {
                const on = categories.includes(cat.id);
                return (
                  <AppTooltip key={cat.id} label={on ? cat.label : `${cat.label} · oculto`}>
                    <button
                      type="button"
                      className={`cal-rail-cat is-${cat.id}${on ? ' is-on' : ''}`}
                      aria-pressed={on}
                      aria-label={cat.label}
                      onClick={() => toggleList(categories, cat.id, setCategories)}
                    >
                      <i className={`cal-cat-dot is-${cat.id}`} />
                    </button>
                  </AppTooltip>
                );
              })}
              <div className="cal-rail-sep" />
              {[
                { id: 'livre', label: 'Horário livre · slots do fotógrafo' },
                { id: 'bloqueado', label: 'Horário bloqueado · indisponível para visitas de fotografia' },
                { id: 'pendente', label: 'Pendente de aprovação · visita à espera do fotógrafo' },
                { id: 'aprovado', label: 'Visita aprovada · fotografia confirmada' },
              ].map(item => (
                <AppTooltip key={item.id} label={item.label}>
                  <span className={`cal-legend-swatch cal-rail-swatch is-${item.id}`} aria-hidden="true" />
                </AppTooltip>
              ))}
            </nav>
            <aside className="cal-aside" aria-hidden={floatFilters && asideCollapsed ? true : undefined}>
              <div className="cal-aside-phone-head">
                <div className="chart-filter-popover-title">Filtros</div>
                <AppTooltip label="Fechar">
                  <button
                    type="button"
                    className="btn-icon-ghost"
                    onClick={() => persistAsideCollapsed(true)}
                    aria-label="Fechar"
                  >
                    <Icon name="close" size={14} />
                  </button>
                </AppTooltip>
              </div>
              <div className="cal-mini">
                <div className="cal-mini-head">
                  <strong>{miniLabel}</strong>
                  <div className="cal-mini-nav">
                    <button type="button" className="btn-icon-ghost" aria-label="Mês anterior" onClick={() => setMiniMonth(d => new Date(d.getFullYear(), d.getMonth() - 1, 1))}>
                      <Icon name="chevronLeft" size={12} />
                    </button>
                    <button type="button" className="btn-icon-ghost" aria-label="Mês seguinte" onClick={() => setMiniMonth(d => new Date(d.getFullYear(), d.getMonth() + 1, 1))}>
                      <Icon name="chevronRight" size={12} />
                    </button>
                  </div>
                </div>
                <div className="cal-mini-weekdays">
                  {CAL_WEEKDAYS.map(d => <span key={d}>{d[0]}</span>)}
                </div>
                <div className="cal-mini-grid">
                  {miniCells.map(d => {
                    const inMonth = d.getMonth() === miniMonth.getMonth();
                    const selected = calSameDay(d, cursor);
                    const isToday = calSameDay(d, today);
                    return (
                      <button
                        key={d.toISOString()}
                        type="button"
                        className={`cal-mini-day${inMonth ? '' : ' is-out'}${selected ? ' is-selected' : ''}${isToday ? ' is-today' : ''}`}
                        onClick={() => { setCursor(new Date(d)); }}
                      >
                        {d.getDate()}
                      </button>
                    );
                  })}
                </div>
              </div>
              {nextEvent && (
                <div className="cal-next">
                  <div className="cal-next-icon"><Icon name={calEventIcon(nextEvent)} size={16} /></div>
                  <div>
                    <strong>{nextEvent.title}</strong>
                    <span>{nextEvent.start.toLocaleDateString('pt-PT', { weekday: 'short', day: 'numeric', month: 'short' })} · {calHm(nextEvent.start)}</span>
                  </div>
                </div>
              )}
              <div className="cal-aside-block">
                <span className="checkbox-group-title">Calendários</span>
                {CAL_SOURCES.map(src => (
                  <label key={src.id} className="avaliacao-check cal-source-row">
                    <input
                      type="checkbox"
                      checked={sources.includes(src.id)}
                      onChange={() => toggleList(sources, src.id, setSources)}
                    />
                    <span className={`cal-source-ico is-${src.id}`}><Icon name={src.icon} size={12} /></span>
                    {src.label}
                  </label>
                ))}
              </div>
              <div className="cal-aside-block">
                <span className="checkbox-group-title">Categorias</span>
                {CAL_CATEGORIES.map(cat => (
                  <button
                    key={cat.id}
                    type="button"
                    className={`cal-cat-row${categories.includes(cat.id) ? ' is-on' : ''}`}
                    onClick={() => toggleList(categories, cat.id, setCategories)}
                  >
                    <i className={`cal-cat-dot is-${cat.id}`} />
                    <Icon name={cat.icon} size={12} />
                    {cat.label}
                  </button>
                ))}
              </div>
              <div className="cal-aside-block">
                <span className="checkbox-group-title">Legenda</span>
                <div className="cal-legend-row">
                  <i className="cal-legend-swatch is-livre" aria-hidden="true" />
                  <span>
                    Horário livre
                    <small>Slots do fotógrafo</small>
                  </span>
                </div>
                <div className="cal-legend-row">
                  <i className="cal-legend-swatch is-bloqueado" aria-hidden="true" />
                  <span>
                    Horário bloqueado
                    <small>Indisponível para visitas de fotografia</small>
                  </span>
                </div>
                <div className="cal-legend-row">
                  <i className="cal-legend-swatch is-pendente" aria-hidden="true" />
                  <span>
                    Pendente de aprovação
                    <small>Visita de fotografia à espera do fotógrafo</small>
                  </span>
                </div>
                <div className="cal-legend-row">
                  <i className="cal-legend-swatch is-aprovado" aria-hidden="true" />
                  <span>
                    Visita aprovada
                    <small>Fotografia confirmada no calendário</small>
                  </span>
                </div>
              </div>
            </aside>
            {!asideCollapsed && (
              <AppTooltip className="cal-aside-toggle-tip" label="Ocultar painel">
                <button
                  type="button"
                  className="cal-aside-toggle"
                  onClick={toggleAside}
                  aria-expanded
                  aria-label="Ocultar painel"
                >
                  <Icon name="chevronLeft" size={12} />
                </button>
              </AppTooltip>
            )}
            </div>
            <div className="cal-main">
              <div className="cal-toolbar">
                <strong className="cal-toolbar-title">{monthLabel}</strong>
                <div className="cal-views">
                  {['mes', 'semana', 'dia'].map(v => (
                    <button
                      key={v}
                      type="button"
                      className={`btn btn-sm ${view === v ? 'btn-primary' : 'btn-secondary'}`}
                      onClick={() => setView(v)}
                    >
                      {v === 'dia' ? 'Dia' : v === 'semana' ? 'Semana' : 'Mês'}
                    </button>
                  ))}
                </div>
                <div className="cal-toolbar-nav">
                <button type="button" className="btn-icon-ghost" onClick={() => shift(-1)} aria-label="Anterior">
                  <Icon name="chevronLeft" size={14} />
                </button>
                <button type="button" className="btn btn-secondary btn-sm" onClick={goToday}>Hoje</button>
                <button type="button" className="btn-icon-ghost" onClick={() => shift(1)} aria-label="Seguinte">
                  <Icon name="chevronRight" size={14} />
                </button>
                </div>
                {canManageSlots && (
                  <div className="cal-toolbar-actions">
                    <button type="button" className="btn btn-secondary btn-sm" onClick={() => openNew(cursor, calWorkStartHour(calSettings), 'slot')}>
                      Adicionar slot
                    </button>
                    <button
                      type="button"
                      className="btn btn-secondary btn-sm"
                      onClick={() => {
                        setDraft(null);
                        setChoice(null);
                        setFormError(null);
                        setWeekMode('livre');
                        const range = calManageableWeekRange(cursor, calSettings);
                        setWeekFrom(calDateInput(range.start));
                        setWeekTo(calDateInput(range.end));
                        setWeekEditingId('');
                        setHoursOpen(false);
                        setWeekOpen(true);
                      }}
                    >
                      Gestão Semanal
                    </button>
                    <button
                      type="button"
                      className="btn btn-secondary btn-sm"
                      onClick={() => {
                        setDraft(null);
                        setChoice(null);
                        setWeekOpen(false);
                        setFormError(null);
                        setHoursOpen(true);
                      }}
                    >
                      Horário de trabalho
                    </button>
                  </div>
                )}
              </div>
              {loadError && <p className="empty-hint cal-load-error">{loadError}</p>}
              {loading && <p className="empty-hint cal-load-error">A carregar calendário…</p>}
              {view === 'mes' ? (
                <div className="cal-month">
                  <div className="cal-month-heads">
                    {CAL_WEEKDAYS.map(d => <div key={d} className="cal-month-head">{isPhone ? d[0] : d}</div>)}
                  </div>
                  {Array.from({ length: 6 }, (_, wi) => {
                    const weekDays = monthCells.slice(wi * 7, wi * 7 + 7);
                    const spanSegs = calMonthSpanSegments(visible.filter(ev => !isCalAvailabilitySlot(ev)), weekDays);
                    const spanLanes = spanSegs.reduce((max, seg) => Math.max(max, seg.lane), 0);
                    return (
                      <div
                        key={weekDays[0].toISOString()}
                        className="cal-month-week"
                        style={{ '--span-lanes': spanLanes }}
                      >
                        {weekDays.map(day => {
                          const inMonth = day.getMonth() === cursor.getMonth();
                          const selected = calSameDay(day, cursor);
                          const isToday = calSameDay(day, today);
                          const dayEvents = displayEventsOn(day);
                          const hasLivre = calDayCanSchedulePhoto(visible, day, calSettings);
                          const hasBlocked = !hasLivre && calDayHasBlocked(visible, day, calSettings);
                          const canSchedulePhoto = hasLivre;
                          const singles = dayEvents.filter(ev => !isCalSpanEvent(ev));
                          const overflow = !isPhone && singles.length > 3;
                          const shown = isPhone ? [] : (overflow ? singles.slice(0, 2) : singles);
                          const extra = singles.length - shown.length;
                          return (
                            <div
                              key={day.toISOString()}
                              className={`cal-month-cell${inMonth ? '' : ' is-out'}${selected ? ' is-selected' : ''}${isToday ? ' is-today' : ''}${!isToday && hasLivre ? ' is-livre' : ''}${!isToday && hasBlocked ? ' is-bloqueado' : ''}`}
                              onClick={() => {
                                if (isPhone && dayEvents.length) {
                                  setDayPeek({ day: new Date(day), events: dayEvents });
                                  return;
                                }
                                openEmpty(day, calFirstBookableHour(day, calSettings));
                              }}
                            >
                              <span className="cal-month-cell-head">
                                <span className={`cal-month-num${isToday ? ' is-today' : ''}`}>{day.getDate()}</span>
                                {isToday && canSchedulePhoto && (
                                  <AppTooltip label="Horário livre — pode pedir visita de fotografia">
                                    <span className="cal-month-livre-dot" aria-hidden="true" />
                                  </AppTooltip>
                                )}
                              </span>
                              {isPhone && dayEvents.length > 0 && (
                                <span className="cal-month-count">{dayEvents.length}</span>
                              )}
                              <div className="cal-month-events">
                                {shown.map(ev => (
                                  <CalendarEventCard
                                    key={ev.id}
                                    ev={ev}
                                    compact
                                    onOpen={openEvent}
                                  />
                                ))}
                                {extra > 0 && (
                                  <button
                                    type="button"
                                    className="cal-month-more"
                                    onClick={(e) => {
                                      e.stopPropagation();
                                      setDayPeek({ day: new Date(day), events: dayEvents });
                                    }}
                                  >
                                    +{extra}
                                  </button>
                                )}
                              </div>
                            </div>
                          );
                        })}
                        {spanSegs.length > 0 && (
                          <div className="cal-month-week-spans">
                            {spanSegs.map(seg => (
                              <div
                                key={seg.ev.id}
                                className={`cal-month-span${seg.clipStart ? ' is-clip-start' : ''}${seg.clipEnd ? ' is-clip-end' : ''}`}
                                style={{ gridColumn: `${seg.startCol} / ${seg.endCol}`, gridRow: seg.lane }}
                              >
                                <CalendarEventCard ev={seg.ev} compact spanPos="bar" onOpen={openEvent} />
                              </div>
                            ))}
                          </div>
                        )}
                      </div>
                    );
                  })}
                </div>
              ) : (
                <div className="cal-grid cal-week-grid" style={{ '--cal-days': days.length, '--cal-hours': CAL_HOURS.length }}>
                  <div className="cal-week-head">
                    <div className="cal-cell cal-head" />
                    {days.map(d => {
                      const selected = calSameDay(d, cursor);
                      const isToday = calSameDay(d, today);
                      return (
                        <div className={`cal-cell cal-head${selected ? ' is-selected' : ''}${isToday ? ' is-today' : ''}`} key={d.toISOString()}>
                          <span className="cal-head-wd">{d.toLocaleDateString('pt-PT', { weekday: 'short' })}</span>
                          <span className="cal-head-num">{d.getDate()}</span>
                        </div>
                      );
                    })}
                  </div>
                  <div className="cal-week-body">
                    <div className="cal-week-times">
                      {CAL_HOURS.map(hour => (
                        <div className="cal-cell cal-time" key={hour}>{String(hour).padStart(2, '0')}:00</div>
                      ))}
                    </div>
                    {days.map(day => {
                      const dayEvents = displayEventsOn(day).filter((ev) => {
                        if (isCalSpanEvent(ev)) return false;
                        const start = calHourDecimal(ev.start);
                        const end = calHourDecimal(ev.end);
                        return end > CAL_HOURS[0] && start < CAL_HOURS[0] + CAL_HOURS.length;
                      });
                      return (
                        <div className="cal-week-day" key={day.toISOString()}>
                          {CAL_HOURS.map(hour => {
                            const wash = calHourSlotWash(visible, day, hour, calSettings);
                            return (
                            <div
                              className={`cal-cell cal-slot${wash.className || ''}`}
                              style={wash.style}
                              key={`${day.toDateString()}-${hour}`}
                              onClick={() => openEmpty(day, hour)}
                            >
                              <span className="cal-slot-add" aria-hidden="true">
                                <Icon name="plus" size={12} />
                              </span>
                            </div>
                            );
                          })}
                          {dayEvents.map(ev => {
                            const box = calWeekEventBox(ev);
                            return (
                              <div
                                key={ev.id}
                                className={`cal-week-event${box.short ? ' is-short' : ''}`}
                                style={{ top: box.top, height: box.height }}
                              >
                                <CalendarEventCard ev={ev} compact={box.short} onOpen={openEvent} />
                              </div>
                            );
                          })}
                        </div>
                      );
                    })}
                  </div>
                </div>
              )}
              {overlayOpen && (
                <div className="cal-event-layer" ref={popoverRef}>
                  {choice && (() => {
                    const hourStart = calAt(choice.day, choice.hour);
                    const hourEnd = calAt(choice.day, choice.hour + 1);
                    const freeSegs = calLivreSegmentsInRange(mergedEvents, hourStart, hourEnd, calSettings);
                    const canPhoto = freeSegs.some(s => s.t1 - s.t0 >= 15 * 60 * 1000);
                    const reason = calPhotoRangeReason(mergedEvents, hourStart, hourEnd, calSettings);
                    return (
                    <CalendarSlotChoice
                      canManageSlots={canManageSlots}
                      coveringSlot={calAvailabilityCovering(
                        mergedEvents,
                        hourStart,
                        hourEnd
                      )}
                      photoAllowed={canPhoto}
                      photoHint={canPhoto
                        ? 'Fotógrafo recebe o pedido para aprovar ou reagendar'
                        : (reason || 'Esse horário já não está livre.')}
                      onEvent={() => openNew(choice.day, choice.hour, 'event')}
                      onPhoto={() => openNew(choice.day, choice.hour, 'photo')}
                      onSlot={() => openNew(choice.day, choice.hour, 'slot')}
                      onEditSlot={() => {
                        const slot = calAvailabilityCovering(
                          mergedEvents,
                          calAt(choice.day, choice.hour),
                          calAt(choice.day, choice.hour + 1)
                        );
                        if (slot) openEvent(slot);
                      }}
                      onClose={closeOverlays}
                    />
                    );
                  })()}
                  {weekOpen && (
                    <CalendarWeekForm
                      from={weekFrom}
                      to={weekTo}
                      setFrom={setWeekFrom}
                      setTo={setWeekTo}
                      mode={weekMode}
                      setMode={setWeekMode}
                      events={events}
                      error={formError}
                      saving={saving}
                      editingId={weekEditingId}
                      onSave={saveWeek}
                      onEdit={editWeekInterval}
                      onDelete={deleteWeekInterval}
                      onClose={closeOverlays}
                    />
                  )}
                  {hoursOpen && (
                    <CalendarWorkHoursForm
                      settings={calSettings}
                      error={formError}
                      saving={saving}
                      onSave={saveWorkHours}
                      onClose={closeOverlays}
                    />
                  )}
                  {dayPeek && (
                    <CalendarDayPeek
                      day={dayPeek.day}
                      events={dayPeek.events}
                      onOpen={(ev) => { setDayPeek(null); openEvent(ev); }}
                      onClose={closeOverlays}
                    />
                  )}
                  {draft && (
                    <CalendarEventForm
                      draft={draft}
                      setDraft={setDraft}
                      ev={draftEvent}
                      error={formError}
                      saving={saving}
                      canManageSlots={canManageSlots}
                      isAdmin={userIsAdmin(user)}
                      calSettings={calSettings}
                      onClose={closeOverlays}
                      onSave={saveDraft}
                      onDelete={deleteDraft}
                      onRequest={requestSlot}
                      onApprove={() => slotAction(`/api/calendar/requests/${encodeURIComponent(draft.id)}/approve`)}
                      onReject={() => slotAction(`/api/calendar/requests/${encodeURIComponent(draft.id)}/reject`)}
                      onReschedule={() => {
                        const start = calParseDateTime(draft.date, draft.startTime);
                        const end = calParseDateTime(draft.endDate || draft.date, draft.endTime);
                        if (!(end > start)) {
                          showCalFormError('A data e hora de fim têm de ser depois do início.');
                          return;
                        }
                        const photoError = calPhotoTimingReason(start, end, calSettings, { allowLunch: true });
                        if (photoError) {
                          showCalFormError(photoError);
                          return;
                        }
                        runCal(() => apiFetch(`/api/calendar/requests/${encodeURIComponent(draft.id)}/reschedule`, {
                          method: 'POST',
                          body: JSON.stringify({ start: calApiDate(start), end: calApiDate(end) }),
                        }));
                      }}
                      onAcceptReschedule={() => slotAction(`/api/calendar/requests/${encodeURIComponent(draft.id)}/accept-reschedule`)}
                      onDeclineReschedule={() => slotAction(`/api/calendar/requests/${encodeURIComponent(draft.id)}/decline-reschedule`)}
                      onCancelVisit={() => {
                        const reason = String(draft.cancelReason || '').trim();
                        if (reason.length < 3) {
                          showCalFormError('Indique o motivo do cancelamento.');
                          return;
                        }
                        runCal(() => apiFetch(`/api/calendar/requests/${encodeURIComponent(draft.id)}/cancel-visit`, {
                          method: 'POST',
                          body: JSON.stringify({ reason }),
                        }));
                      }}
                      onLock={() => slotAction(`/api/calendar/slots/${encodeURIComponent(draft.id)}/lock`)}
                      onUnlock={() => slotAction(`/api/calendar/slots/${encodeURIComponent(draft.id)}/unlock`)}
                      onError={showCalFormError}
                    />
                  )}
                </div>
              )}
            </div>
          </div>
        </div>
      </div>
    </>
  );
}

const DASH_MONTH_OPTIONS = [
  { value: '1', label: 'Jan' },
  { value: '2', label: 'Fev' },
  { value: '3', label: 'Mar' },
  { value: '4', label: 'Abr' },
  { value: '5', label: 'Mai' },
  { value: '6', label: 'Jun' },
  { value: '7', label: 'Jul' },
  { value: '8', label: 'Ago' },
  { value: '9', label: 'Set' },
  { value: '10', label: 'Out' },
  { value: '11', label: 'Nov' },
  { value: '12', label: 'Dez' },
];

function appendDashPeriod(params, ano, mes) {
  if (ano) params.set('ano', String(ano));
  if (mes) params.set('mes', String(mes));
  return params;
}

function dashPeriodQuery(ano, mes) {
  const qs = appendDashPeriod(new URLSearchParams(), ano, mes).toString();
  return qs ? `?${qs}` : '';
}

function DashboardPeriodSelect({ label, value, options, onChange, ariaLabel, className = '' }) {
  return (
    <AppSelect
      variant="pill"
      label={label}
      value={value}
      options={options}
      onChange={onChange}
      ariaLabel={ariaLabel}
      placeholder="Todos"
      className={className}
    />
  );
}

function DashboardPage({ user, actorUser = null, enabledModules, isViewingAs = false, onNavigate }) {
  const { isPhone } = useViewportNav();
  const [showNotifications, setShowNotifications] = useState(false);
  const [notifItems, setNotifItems] = useState([]);
  const [notifLoading, setNotifLoading] = useState(false);
  const [notifError, setNotifError] = useState(null);
  const [releaseSeen, setReleaseSeen] = useState(() => hasSeenRelease(APP_RELEASE.id));
  const [sendingLembretes, setSendingLembretes] = useState(false);
  const [editMode, setEditMode] = useState(false);
  const [kpis, setKpis] = useState([]);
  const [pipeline, setPipeline] = useState([]);
  const [pipelineFilterHint, setPipelineFilterHint] = useState('');
  const [dashAno, setDashAno] = useState(() => String(new Date().getFullYear()));
  const [dashMes, setDashMes] = useState(() => String(new Date().getMonth() + 1));
  const [activities, setActivities] = useState({ today: [], recent: [], todayFilterHint: '', recentFilterHint: '' });
  const [extraCards, setExtraCards] = useState(null);
  const [extraCardsLoading, setExtraCardsLoading] = useState(true);
  const [kpisLoading, setKpisLoading] = useState(true);
  const [pipelineLoading, setPipelineLoading] = useState(true);
  const [activitiesLoading, setActivitiesLoading] = useState(true);
  const [inspectWidget, setInspectWidget] = useState(null);
  const [dashDataKey, setDashDataKey] = useState(0);
  const [showSaveDefaultModal, setShowSaveDefaultModal] = useState(false);
  const [savingDefaultLayout, setSavingDefaultLayout] = useState(false);
  const persistLayoutTimer = useRef(null);
  const saveDefaultDismiss = useOverlayDismiss(() => {
    if (!savingDefaultLayout) setShowSaveDefaultModal(false);
  });

  const hasMediacao = enabledModules.includes('mediacao');
  const hasSoi = enabledModules.includes('soi');
  const hasAngariacao = enabledModules.includes('angariacao');
  const hasIntermediacao = enabledModules.includes('intermediacao');
  const canInspectDashboard = userIsAdmin(user);
  const canEditPhoneLayout = canInspectDashboard;

  useEffect(() => {
    if (isPhone && !canEditPhoneLayout) setEditMode(false);
  }, [isPhone, canEditPhoneLayout]);
  const userKey = user?.email || user?.Email || user?.displayName || user?.DisplayName || '';
  const dashCtx = useMemo(
    () => dashboardContext({ hasMediacao, hasSoi, hasAngariacao, hasIntermediacao, user }),
    [hasMediacao, hasSoi, hasAngariacao, hasIntermediacao, user?.existeComissao, user?.ExisteComissao, user?.objectivo, user?.Objectivo, user?.sumComissao, user?.SumComissao, user?.isAdmin, user?.IsAdmin, user?.roles, user?.Roles]
  );
  const [layout, setLayout] = useState(() => normalizeDashboardLayout(loadDashboardLayout(userKey), dashCtx));

  useEffect(() => () => {
    if (persistLayoutTimer.current) window.clearTimeout(persistLayoutTimer.current);
  }, []);
  const dashYearOptions = (() => {
    const y = new Date().getFullYear();
    return [y - 2, y - 1, y, y + 1];
  })();
  const dashAnoQuery = dashPeriodQuery(dashAno, dashMes);

  useEffect(() => {
    setLayout(normalizeDashboardLayout(loadDashboardLayout(userKey), dashCtx));
  }, [userKey]);

  // Sync forced server layouts (admin "replace all") and cache the global default locally.
  useEffect(() => {
    if (!userKey) return undefined;
    let cancelled = false;
    (async () => {
      let appliedForce = false;
      try {
        const def = await apiFetch('/api/dashboard/default-layout');
        const widgets = def?.widgets || def?.Widgets || [];
        const forceAllAt = def?.forceAllAt || def?.ForceAllAt || null;
        const savedAt = def?.savedAt || def?.SavedAt || null;
        if (!cancelled && Array.isArray(widgets) && widgets.length) {
          saveDashboardLayout('default', { widgets, savedAt, forceAllAt });
          if (forceAllAt) {
            let lastSync = null;
            try { lastSync = window.localStorage.getItem(dashboardLayoutSyncedAtKey(userKey)); } catch { /* */ }
            if (!lastSync || new Date(forceAllAt).getTime() > new Date(lastSync).getTime()) {
              saveDashboardLayout(userKey, { widgets });
              try { window.localStorage.setItem(dashboardLayoutSyncedAtKey(userKey), String(forceAllAt)); } catch { /* */ }
              setLayout(normalizeDashboardLayout({ widgets }, dashCtx));
              appliedForce = true;
              try {
                await apiFetch('/api/me/preferences', {
                  method: 'PUT',
                  body: JSON.stringify({
                    ...normalizeUserPreferences(loadLocalUserPreferences({ email: userKey })),
                    dashboard: { layoutSavedAt: forceAllAt, widgets },
                  }),
                });
              } catch { /* prefs sync best-effort */ }
            }
          }
        }
      } catch { /* ignore */ }

      if (appliedForce || cancelled) return;

      try {
        const prefs = await apiFetch('/api/me/preferences');
        const widgets = prefs?.dashboard?.widgets || prefs?.Dashboard?.Widgets || null;
        const savedAt = prefs?.dashboard?.layoutSavedAt || prefs?.Dashboard?.LayoutSavedAt || null;
        if (!cancelled && Array.isArray(widgets) && widgets.length && savedAt) {
          let lastSync = null;
          try { lastSync = window.localStorage.getItem(dashboardLayoutSyncedAtKey(userKey)); } catch { /* */ }
          if (!lastSync || new Date(savedAt).getTime() > new Date(lastSync).getTime()) {
            const local = loadDashboardLayout(userKey);
            const merged = mergeKeptDashboardWidgets(widgets, local?.widgets);
            saveDashboardLayout(userKey, { widgets: merged });
            try { window.localStorage.setItem(dashboardLayoutSyncedAtKey(userKey), String(savedAt)); } catch { /* */ }
            setLayout(normalizeDashboardLayout({ widgets: merged }, dashCtx));
          }
        }
      } catch { /* offline / first visit */ }
    })();
    return () => { cancelled = true; };
  }, [userKey, dashCtx]);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      setNotifLoading(true);
      setNotifError(null);
      try {
        const data = await apiFetch('/api/notificacoes/me');
        if (!cancelled) setNotifItems(data.items || data.Items || []);
      } catch (err) {
        if (!cancelled) {
          setNotifItems([]);
          setNotifError(err.message || 'Não foi possível carregar notificações.');
        }
      } finally {
        if (!cancelled) setNotifLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, [userKey, dashDataKey]);

  useEffect(() => {
    const onSeen = () => setReleaseSeen(hasSeenRelease(APP_RELEASE.id));
    window.addEventListener('myhabita:release-seen', onSeen);
    return () => window.removeEventListener('myhabita:release-seen', onSeen);
  }, []);

  const displayNotifItems = useMemo(
    () => withLatestReleaseNotification(notifItems),
    [notifItems, releaseSeen]
  );

  useEffect(() => {
    if (releaseSeen) return undefined;
    window.dispatchEvent(new CustomEvent('myhabita:open-release-notes'));
    return undefined;
  }, [releaseSeen]);

  useEffect(() => {
    setLayout(prev => normalizeDashboardLayout({ widgets: prev.widgets, v: prev.v || DASHBOARD_LAYOUT_VERSION }, dashCtx));
  }, [hasMediacao, hasSoi, dashCtx.hasCommission, dashCtx.hasConsultor, dashCtx.isAdmin]);

  useEffect(() => {
    if (!userKey) return;
    saveDashboardLayout(userKey, layout);
  }, [layout, userKey]);

  useEffect(() => {
    if (!editMode || !isPhone || !canEditPhoneLayout) return;
    setLayout(prev => {
      const widgets = prev.widgets || [];
      if (widgets.length && widgets.every(w => readWidgetPhoneOrder(w) != null)) return prev;
      return { widgets: assignPhoneOrder(widgets) };
    });
  }, [editMode, isPhone, canEditPhoneLayout]);

  useEffect(() => {
    let cancelled = false;

    if (!hasMediacao) {
      setKpisLoading(false);
      setActivitiesLoading(false);
      setPipelineLoading(false);
      setPipeline([]);
      setPipelineFilterHint('');
      setExtraCardsLoading(false);
      setExtraCards(null);
    } else {
      setKpisLoading(true);
      setActivitiesLoading(true);
      setPipelineLoading(true);

      apiFetch(`/api/dashboard/kpis${dashAnoQuery}`)
        .then(res => { if (!cancelled) setKpis(res.items || res.Items || []); })
        .catch(() => { if (!cancelled) setKpis([]); })
        .finally(() => { if (!cancelled) setKpisLoading(false); });

      Promise.all([
        apiFetch(`/api/dashboard/activities${dashAnoQuery}`).catch(() => ({})),
        loadWeekActivities(dashAno, dashMes),
      ]).then(([res, week]) => {
          if (!cancelled) {
            setActivities({
              today: week,
              recent: res.recent || res.Recent || [],
              todayFilterHint: res.todayFilterHint || res.TodayFilterHint || '',
              recentFilterHint: res.recentFilterHint || res.RecentFilterHint || '',
            });
          }
        })
        .catch(() => { if (!cancelled) setActivities({ today: [], recent: [], todayFilterHint: '', recentFilterHint: '' }); })
        .finally(() => { if (!cancelled) setActivitiesLoading(false); });

      apiFetch(`/api/dashboard/pipeline${dashAnoQuery}`)
        .then(res => {
          if (!cancelled) {
            setPipeline(res.columns || res.Columns || []);
            setPipelineFilterHint(res.filterHint || res.FilterHint || '');
          }
        })
        .catch(() => {
          if (!cancelled) {
            setPipeline([]);
            setPipelineFilterHint('');
          }
        })
        .finally(() => { if (!cancelled) setPipelineLoading(false); });

      setExtraCardsLoading(true);
      apiFetch(`/api/dashboard/extra-cards${dashAnoQuery}`)
        .then(res => { if (!cancelled) setExtraCards(res); })
        .catch(() => { if (!cancelled) setExtraCards(null); })
        .finally(() => { if (!cancelled) setExtraCardsLoading(false); });
    }

    return () => { cancelled = true; };
  }, [hasMediacao, hasSoi, userKey, dashAno, dashMes, dashDataKey]);

  const goMediacao = (tab, filters) => {
    if (onNavigate) onNavigate('mediacao', tab, null, filters);
  };
  const mediacaoPeriod = (extra = {}) => {
    const now = new Date();
    const filters = { ...extra };
    if (dashAno) filters.ano = Number(dashAno) || now.getFullYear();
    if (dashMes) filters.mes = Number(dashMes) || now.getMonth() + 1;
    return filters;
  };
  const comissaoPeriod = (extra = {}) => {
    const now = new Date();
    const filters = { ...extra };
    if (dashAno) filters.ano = Number(dashAno) || now.getFullYear();
    return filters;
  };
  const handleNewDeal = () => goMediacao('negocios', { novo: '1' });
  const handleExploreKpi = (kpi) => {
    const label = (kpi?.label ?? kpi?.Label ?? '').toLowerCase();
    const isValor = label.includes('valor');
    let estado = '';
    if (label.includes('pipeline')) estado = 'Em Curso';
    else if (label.includes('ganhos') || isValor) estado = 'Concluído';
    else if (label.includes('comissão') || label.includes('comissao')) estado = 'Em Curso';
    if (isValor) {
      goMediacao('faturacao', mediacaoPeriod());
      return;
    }
    if (label.includes('comissão') || label.includes('comissao')) {
      goMediacao('negocios', mediacaoPeriod({ estado }));
      return;
    }
    goMediacao('negocios', mediacaoPeriod({ estado }));
  };
  const displayKpis = kpis.length ? kpis : KPI_PLACEHOLDERS;

  const visibleWidgetIds = new Set(layout.widgets.map(w => w.id));
  const hiddenWidgetIds = getAvailableDashboardWidgetIds(dashCtx).filter(id => !visibleWidgetIds.has(id));

  const handleLayoutChange = (widgets) => {
    const next = pinAvaliacaoWidgetFirst(widgets);
    setLayout({ widgets: next });
    try {
      window.localStorage.setItem(dashboardLayoutSyncedAtKey(userKey), new Date().toISOString());
    } catch { /* */ }
    if (persistLayoutTimer.current) window.clearTimeout(persistLayoutTimer.current);
    persistLayoutTimer.current = window.setTimeout(() => {
      persistDashboardLayout(user, next);
    }, 700);
  };

  const handleAddWidget = (id) => {
    const def = DASHBOARD_WIDGET_DEFS[id];
    if (!def) return;
    const base = assignPhoneOrder(layout.widgets);
    const spot = findFreeDashboardSpot(base, def.defaultW, def.defaultH);
    const nextWidget = clampDashboardWidget({
      id,
      x: spot.x,
      y: spot.y,
      w: def.defaultW,
      h: def.defaultH,
      phoneOrder: base.length,
    });
    const next = [...base, nextWidget];
    handleLayoutChange(isPhone
      ? assignPhoneOrder(next)
      : compactDashboardLayout(placeDashboardWidget(next, id, { x: spot.x, y: spot.y, w: def.defaultW, h: def.defaultH })));
  };

  const handleResetLayout = async () => {
    try {
      const next = await fetchDashboardDefaultLayout(dashCtx);
      const widgets = Array.isArray(next?.widgets) ? next.widgets : (Array.isArray(next) ? next : []);
      if (!widgets.length) {
        toast.warning('Ainda não existe uma predefinição guardada.');
        return;
      }
      handleLayoutChange(widgets);
      toast.success('Layout reposto para a predefinição guardada.');
    } catch (err) {
      toast.error(err?.message || 'Não foi possível repor a predefinição.');
    }
  };

  const handleSaveAsDefaultLayout = async (replaceAllUsers = false) => {
    const widgets = serializeDashboardWidgets(layout.widgets);
    if (!widgets.length) {
      toast.warning('O layout atual não tem widgets para guardar.');
      setShowSaveDefaultModal(false);
      return;
    }

    setSavingDefaultLayout(true);
    toast.info(replaceAllUsers ? 'A guardar e substituir dashboards…' : 'A guardar predefinição…');
    try {
      const result = await apiFetch('/api/dashboard/default-layout', {
        method: 'PUT',
        body: JSON.stringify({
          widgets,
          replaceAllUsers: Boolean(replaceAllUsers),
        }),
      });
      const savedWidgets = result.widgets || result.Widgets || widgets;
      const savedAt = result.savedAt || result.SavedAt || new Date().toISOString();
      saveDashboardLayout('default', {
        widgets: savedWidgets,
        savedAt,
      });
      if (replaceAllUsers) {
        const forceAt = result.forceAllAt || result.ForceAllAt || savedAt;
        saveDashboardLayout(userKey, { widgets: savedWidgets });
        try {
          window.localStorage.setItem(dashboardLayoutSyncedAtKey(userKey), String(forceAt));
        } catch { /* */ }
      }
      toast.success(
        replaceAllUsers
          ? 'Predefinição guardada e aplicada a todos os utilizadores (na próxima abertura do dashboard).'
          : 'Predefinição guardada. “Repor predefinição” usa este layout.'
      );
      setShowSaveDefaultModal(false);
    } catch (e) {
      // Stale API without the endpoint: still cache locally so Repor works in this browser.
      if (e?.status === 404 || e?.status === 405 || e?.code === 'endpoint_missing') {
        const savedAt = new Date().toISOString();
        saveDashboardLayout('default', { widgets, savedAt });
        toast.warning(
          'API desatualizada — predefinição guardada só neste browser. Reinicie a API para sincronizar no servidor.'
        );
        setShowSaveDefaultModal(false);
      } else {
        toast.error(e?.message || 'Não foi possível guardar a predefinição do dashboard.');
      }
    } finally {
      setSavingDefaultLayout(false);
    }
  };

  const openInspect = (widgetId) => {
    if (!canInspectDashboard) return;
    setInspectWidget(widgetId);
  };

  const renderDashboardWidget = (id) => {
    const def = DASHBOARD_WIDGET_DEFS[id];
    const dashPeriod = dashboardPeriodLabel(dashAno, dashMes);
    // Inspect eye lives only in the widget chrome (Personalizar). Do not duplicate inside cards.
    const inspectBtn = null;
    if (id === 'kpi-pipeline') {
      return (
        <OperacaoBarsCard
          ano={dashAno}
          mes={dashMes}
          title="Negócios em Pipeline"
          metric="count"
          estados={['Em Curso']}
          layout="numbers"
          icon="home"
          onExplore={() => goMediacao('negocios', mediacaoPeriod({ estado: 'Em Curso' }))}
        />
      );
    }
    if (id === 'kpi-ganhos') {
      return (
        <OperacaoBarsCard
          ano={dashAno}
          mes={dashMes}
          title="Negócios Ganhos"
          metric="count"
          estados={['Concluído']}
          probabilidades={[]}
          showProbFilter={false}
          onExplore={() => goMediacao('negocios', mediacaoPeriod({ estado: 'Concluído' }))}
        />
      );
    }
    if (id === 'kpi-valor') {
      return (
        <FaturacaoChartsCard
          ano={dashAno}
          mes={dashMes}
          onExplore={() => goMediacao('faturacao', mediacaoPeriod())}
        />
      );
    }
    if (def?.kpiIndex != null) {
      const kpi = displayKpis[def.kpiIndex] || KPI_PLACEHOLDERS[def.kpiIndex];
      const icons = ['home', 'graph', 'thumb', 'graph'];
      return (
        <DashboardKpiCard
          kpi={kpi}
          icon={icons[def.kpiIndex] || 'graph'}
          loading={kpisLoading}
          period={dashPeriod}
          onExplore={() => handleExploreKpi(kpi)}
          onInspect={inspectBtn}
        />
      );
    }
    switch (id) {
      case 'pipeline':
        return (
          <PipelineCard
            ano={dashAno}
            mes={dashMes}
            refreshKey={dashDataKey}
            onInspect={inspectBtn}
            onExplore={() => goMediacao('negocios', mediacaoPeriod({ estado: 'Em Curso' }))}
          />
        );
      case 'progress':
      case 'comissao':
        return (
          <ComissaoProgressCard
            user={user}
            ano={dashAno}
            onExplore={() => goMediacao('comissao', comissaoPeriod())}
          />
        );
      case 'challenges':
        return <ChallengeList pipeline={pipeline} loading={pipelineLoading} />;
      case 'leaderboard':
        return <Leaderboard ano={dashAno} mes={dashMes} user={user} />;
      case 'operacao-chart':
        return <OperacaoPieChartCard ano={dashAno} periodMes={dashMes} user={user} isViewingAs={isViewingAs} onInspect={inspectBtn} onExplore={() => goMediacao('comissao', comissaoPeriod())} />;
      case 'activities-today':
        return (
          <DashCard
            icon="activity"
            title={(
              <>
                Atividades da Semana
                <DashboardFilterStar hint={activities.todayFilterHint} />
              </>
            )}
            period={dashPeriod}
            onExplore={() => onNavigate?.('calendario')}
          >
            <ActivityList items={activities.today} emptyLabel="Sem atividades neste mês." loading={activitiesLoading} />
          </DashCard>
        );
      case 'activities-recent':
        return (
          <DashCard
            icon="activity"
            title={(
              <>
                Atividade Recente
                <DashboardFilterStar hint={activities.recentFilterHint} />
              </>
            )}
            period={dashPeriod}
            onExplore={() => onNavigate?.('calendario')}
          >
            <ActivityList items={activities.recent} emptyLabel="Sem atividade recente." loading={activitiesLoading} />
          </DashCard>
        );
      case 'kpi-angariacao':
        return (
          <EstadoBucketsCard
            title="Angariações"
            icon="angariacao"
            buckets={extraCards?.angariacao || extraCards?.Angariacao}
            loading={extraCardsLoading}
            period={dashPeriod}
            onExplore={() => onNavigate?.('angariacao')}
          />
        );
      case 'kpi-intermediacao':
        return (
          <EstadoBucketsCard
            title="Intermediação Crédito"
            icon="bank"
            buckets={extraCards?.intermediacao || extraCards?.Intermediacao}
            loading={extraCardsLoading}
            period={dashPeriod}
            onExplore={() => onNavigate?.('intermediacao')}
          />
        );
      case 'kpi-pipeline-alto':
        return (
          <OperacaoBarsCard
            ano={dashAno}
            mes={dashMes}
            title="Pipeline Em Curso Alto"
            metric="count"
            estados={['Em Curso']}
            probabilidades={['Alta']}
            showProbFilter={false}
            onExplore={() => goMediacao('negocios', mediacaoPeriod({ estado: 'Em Curso', probabilidade: 'Alta' }))}
          />
        );
      case 'kpi-mix-natureza':
        return <MixNaturezaCard ano={dashAno} mes={dashMes} onExplore={() => goMediacao('negocios', mediacaoPeriod())} />;
      case 'kpi-comissao-forecast':
        return (
          <ComissaoForecastCard
            ano={dashAno}
            mes={dashMes}
            onExplore={() => goMediacao('negocios', mediacaoPeriod())}
          />
        );
      case 'soi':
        return hasSoi ? (
          <SoiScoreCard ano={dashAno} mes={dashMes} onExplore={() => onNavigate?.('soi')} />
        ) : (
          <DashCard icon="soi" title="SOI">
            <p className="empty-hint">Módulo SOI desativado.</p>
          </DashCard>
        );
      case 'avaliacao-trimestral':
        return null;
      default:
        return null;
    }
  };

  const yearFilter = hasMediacao ? (
    <div className="dashboard-period-filters">
      <DashboardPeriodSelect
        label="Ano"
        value={dashAno}
        ariaLabel="Filtrar dashboard por ano"
        options={[{ value: '', label: 'Todos' }, ...dashYearOptions.map(y => ({ value: String(y), label: String(y) }))]}
        onChange={setDashAno}
      />
      <DashboardPeriodSelect
        label="Mês"
        value={dashMes}
        ariaLabel="Filtrar dashboard por mês"
        options={[{ value: '', label: 'Todos' }, ...DASH_MONTH_OPTIONS]}
        onChange={setDashMes}
      />
    </div>
  ) : null;

  const layoutEditButtons = editMode ? (
    <>
      <button type="button" className="btn btn-secondary btn-sm" onClick={handleResetLayout}>
        Repor predefinição
      </button>
      {canInspectDashboard && (
        <button
          type="button"
          className="btn btn-primary btn-sm"
          onClick={() => setShowSaveDefaultModal(true)}
          disabled={savingDefaultLayout}
        >
          {savingDefaultLayout ? 'A guardar…' : 'Guardar predefinição'}
        </button>
      )}
      <button
        type="button"
        className="btn btn-primary btn-sm"
        onClick={() => {
          persistDashboardLayout(user, layout.widgets);
          setEditMode(false);
        }}
      >
        Concluir
      </button>
    </>
  ) : (
    <button type="button" className="btn btn-secondary btn-sm" onClick={() => setEditMode(true)}>
      <Icon name="layout" size={14} />
      Personalizar
    </button>
  );

  const headerExtra = (
    <>
      {yearFilter}
      {!isPhone && layoutEditButtons}
    </>
  );

  return (
    <>
      <ModuleTopbar
        user={user}
        dashboardGreeting
        headerExtra={headerExtra}
        onOpenNotifications={() => setShowNotifications(v => !v)}
        notificationsOpen={showNotifications}
        onCloseNotifications={() => setShowNotifications(false)}
        notificationCount={displayNotifItems.filter(n => n.requiresAction ?? n.RequiresAction).length}
        notificationsPanel={(
          <NotificationsPanel
            items={displayNotifItems}
            loading={notifLoading && !displayNotifItems.length}
            error={displayNotifItems.length ? null : notifError}
            isAdmin={canInspectDashboard}
            sendingLembretes={sendingLembretes}
            onOpenRelease={() => {
              setShowNotifications(false);
              window.dispatchEvent(new CustomEvent('myhabita:open-release-notes'));
            }}
            onOpenAvaliacao={() => {
              setShowNotifications(false);
              window.dispatchEvent(new CustomEvent('myhabita:open-avaliacao'));
            }}
            onOpenCalendar={(n) => {
              setShowNotifications(false);
              const eventId = calNotifEventId(n);
              if (eventId) {
                try { sessionStorage.setItem('myhabita.cal-open-event', eventId); } catch { /* ignore */ }
              }
              onNavigate?.('calendario');
            }}
            onSendLembretes={async () => {
              setSendingLembretes(true);
              try {
                const result = await apiFetch('/api/notificacoes/avaliacao/lembretes', { method: 'POST' });
                const sent = result.sent ?? result.Sent ?? 0;
                const failed = result.failed ?? result.Failed ?? 0;
                if (failed > 0) {
                  toast.warning(`Lembretes enviados: ${sent}. Falhas: ${failed}.`);
                } else {
                  toast.success(`Lembretes enviados: ${sent}.`);
                }
                setDashDataKey(k => k + 1);
              } catch (err) {
                toast.error(err.message || 'Erro ao enviar lembretes.');
              } finally {
                setSendingLembretes(false);
              }
            }}
          />
        )}
        onNewDeal={hasMediacao ? handleNewDeal : null}
      />
      <AvaliacaoTrimestralCard user={user} actorUser={actorUser || user} />
      <div className="page-body">
        <div className="page-content dashboard-v2-layout">
          {isPhone && canEditPhoneLayout && !editMode && (
            <div className="dashboard-phone-admin-bar">
              <button type="button" className="btn btn-secondary btn-sm" onClick={() => setEditMode(true)}>
                <Icon name="layout" size={14} />
                Personalizar cartões
              </button>
            </div>
          )}
          {editMode && (!isPhone || canEditPhoneLayout) && (
            <div className="dashboard-edit-banner" role="status">
              <span>
                {isPhone
                  ? 'Modo de edição (telemóvel): setas para reordenar, + / − ou o canto verde para aumentar os cartões. Depois escolha Guardar predefinição.'
                  : 'Modo de edição: arraste os cartões para mover, use o canto inferior direito para redimensionar.'}
              </span>
              {isPhone && canEditPhoneLayout && (
                <div className="dashboard-edit-banner-actions">
                  {layoutEditButtons}
                </div>
              )}
              {hiddenWidgetIds.length > 0 && (
                <div className="dashboard-add-widgets">
                  <span>Adicionar:</span>
                  {hiddenWidgetIds.map(id => (
                    <button
                      key={id}
                      type="button"
                      className="btn btn-secondary btn-sm"
                      onClick={() => handleAddWidget(id)}
                    >
                      <Icon name="plus" size={12} />
                      {DASHBOARD_WIDGET_DEFS[id]?.label || id}
                    </button>
                  ))}
                </div>
              )}
            </div>
          )}
          <div className="dashboard-v2-scroll">
            <DashboardWidgetGrid
              widgets={layout.widgets}
              editMode={editMode && (!isPhone || canEditPhoneLayout)}
              onChange={handleLayoutChange}
              renderWidget={renderDashboardWidget}
              inspectEnabled={canInspectDashboard}
              onInspectWidget={openInspect}
              canEditPinnedWidgets={canInspectDashboard}
            />
          </div>
        </div>
        {inspectWidget && (
          <DashboardInspectModal
            widgetId={inspectWidget}
            widgetLabel={DASHBOARD_WIDGET_DEFS[inspectWidget]?.label || inspectWidget}
            ano={dashAno}
            mes={dashMes}
            onClose={() => setInspectWidget(null)}
            onFilterSaved={() => setDashDataKey(k => k + 1)}
          />
        )}
        {showSaveDefaultModal && ReactDOM.createPortal(
          <div
            className="confirm-dialog-overlay"
            role="presentation"
            {...saveDefaultDismiss}
          >
            <div
              className="confirm-dialog confirm-dialog-choices"
              role="alertdialog"
              aria-modal="true"
              aria-labelledby="save-default-title"
              aria-describedby="save-default-msg"
              onClick={e => e.stopPropagation()}
            >
              <h3 id="save-default-title">Guardar predefinição</h3>
              <p id="save-default-msg">
                Como pretende guardar o layout atual do dashboard?
              </p>
              <div className="confirm-dialog-choice-list">
                <button
                  type="button"
                  className="btn btn-primary"
                  disabled={savingDefaultLayout}
                  onClick={() => handleSaveAsDefaultLayout(false)}
                >
                  Só guardar predefinição
                </button>
                <button
                  type="button"
                  className="btn btn-primary-accent"
                  disabled={savingDefaultLayout}
                  onClick={() => handleSaveAsDefaultLayout(true)}
                >
                  Guardar e substituir todos os dashboards
                </button>
                <button
                  type="button"
                  className="btn btn-secondary"
                  disabled={savingDefaultLayout}
                  onClick={() => setShowSaveDefaultModal(false)}
                >
                  Cancelar
                </button>
              </div>
              <p className="confirm-dialog-hint">
                <strong>Só predefinição</strong> define o layout usado por “Repor predefinição”, incluindo a ordem dos cartões no telemóvel.
                {' '}
                <strong>Substituir todos</strong> também atualiza o dashboard guardado de cada utilizador.
              </p>
            </div>
          </div>,
          document.body
        )}
      </div>
    </>
  );
}

function fieldValue(fields, ...keys) {
  if (!fields) return null;
  for (const key of keys) {
    if (fields[key] != null) return fields[key];
    const match = Object.keys(fields).find(k => k.toLowerCase() === key.toLowerCase());
    if (match && fields[match] != null) return fields[match];
  }
  return null;
}

function memberField(member, ...keys) {
  for (const key of keys) {
    if (member?.[key] != null && member[key] !== '') return member[key];
    const cap = key.charAt(0).toUpperCase() + key.slice(1);
    if (member?.[cap] != null && member[cap] !== '') return member[cap];
  }
  return null;
}

function memberAssinatura(member) {
  return memberField(member, 'assinatura', 'nome', 'Nome') || '—';
}

/** Sort rank for Equipa cards: Direção → others. */
function equipaFuncaoRank(funcao) {
  const f = String(funcao || '')
    .toLowerCase()
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '');
  // Matches both PT "diretor" and EN "director".
  if (/direc?tor\s+geral/.test(f)) return 0;
  if (/direc?tor\s+comercial/.test(f)) return 1;
  if (/gestor(?:a)?\s+de\s+negocios/.test(f)) return 2;
  if (/direc?tor/.test(f)) return 3;
  if (/consultor/.test(f)) return 4;
  return 5;
}

function equipaIsDirecao(funcao) {
  return equipaFuncaoRank(funcao) <= 3;
}

function equipaIsDiretorGeral(funcao) {
  return equipaFuncaoRank(funcao) === 0;
}

function equipaIsCanalHabita(member) {
  const name = memberAssinatura(member);
  const email = memberEmail(member);
  return /canal\s*habita/i.test(name) || /canal\s*habita/i.test(email);
}

/**
 * Equipa card order: direção first → commission stars (1º…3º) → alphabet.
 * @param {(m: object) => number} commissionRankOf 1–3 for top commission, else 0
 */
function compareEquipaMembers(a, b, commissionRankOf = () => 0) {
  const dirA = equipaIsDirecao(memberField(a, 'funcao', 'Funcao')) ? 0 : 1;
  const dirB = equipaIsDirecao(memberField(b, 'funcao', 'Funcao')) ? 0 : 1;
  if (dirA !== dirB) return dirA - dirB;

  const starA = commissionRankOf(a) || 99;
  const starB = commissionRankOf(b) || 99;
  if (starA !== starB) return starA - starB;

  return memberAssinatura(a).localeCompare(memberAssinatura(b), 'pt', { sensitivity: 'base' });
}

function memberEmail(member) {
  return memberField(member, 'email', 'Email', 'correioEletronico', 'CorreioEletronico');
}

function memberHasSharePointImageColumn(member, ...columnNames) {
  const fields = member?.fields || member?.Fields || member || {};
  const want = columnNames.map(n => String(n).toLowerCase());
  for (const [key, raw] of Object.entries(fields)) {
    if (!want.includes(String(key).toLowerCase())) continue;
    if (raw == null || raw === '') continue;
    if (typeof raw === 'string') {
      if (raw.trim().startsWith('{') && /fileName|serverRelativeUrl|thumbnailRenderer/i.test(raw)) return true;
      if (/\.(png|jpe?g|gif|webp|bmp)$/i.test(raw)) return true;
      continue;
    }
    if (typeof raw === 'object') {
      if (raw.fileName || raw.serverRelativeUrl || raw.thumbnailRenderer) return true;
      try {
        if (/fileName|serverRelativeUrl|thumbnailRenderer/i.test(JSON.stringify(raw))) return true;
      } catch { /* ignore */ }
    }
  }
  return false;
}

function memberHasSharePointImagem(member) {
  return memberHasSharePointImageColumn(member, 'Imagem', 'imagem', 'Foto', 'foto', 'Image', 'Photo')
    || Boolean(member?.imagemVersion || member?.ImagemVersion);
}

function memberHasImagemPerfil(member) {
  return memberHasSharePointImageColumn(member, 'ImagemdePerfil', 'ImagemDePerfil', 'Imagem de Perfil')
    || Boolean(member?.imagemPerfilVersion || member?.ImagemPerfilVersion);
}

function memberColaboradoresImageUrl(member, column, size, version) {
  const id = member?.id || member?.Id;
  if (!id || !column) return null;
  const qs = `size=${encodeURIComponent(size)}${version ? `&v=${encodeURIComponent(version)}` : ''}&r=5`;
  return `/api/lists/himo-colaboradores/items/${encodeURIComponent(id)}/image/${encodeURIComponent(column)}?${qs}`;
}

/** Colaboradores «Imagem» — same field as the Equipa list column. Never Graph. */
function memberColaboradorImageUrl(member, size = 'smallSquare') {
  const id = member?.id || member?.Id;
  if (!id) return null;
  const version = member.imagemVersion || member.ImagemVersion || '';
  return memberColaboradoresImageUrl(member, 'Imagem', size, version);
}

/** Colaboradores «Imagem de Perfil» — flipped card face only. Use large so the cut-out stays sharp. */
function memberImagemPerfilUrl(member, size = 'large') {
  const id = member?.id || member?.Id;
  if (!id) return null;
  const version = member.imagemPerfilVersion || member.ImagemPerfilVersion || '';
  return memberColaboradoresImageUrl(member, 'ImagemdePerfil', size, version);
}

function memberAdmissionDate(member) {
  const raw = member?.dataAdmissao || member?.DataAdmissao
    || memberField(member, 'DataAdmiss_x00e3_o', 'DataAdmissao', 'Data de Admissão', 'DataEntrada');
  if (!raw) return null;
  const d = new Date(raw);
  return Number.isNaN(d.getTime()) ? null : d;
}

function memberAntiguidadeLabel(member) {
  return memberField(member, 'antiguidade', 'Antiguidade')
    || member?.antiguidade
    || member?.Antiguidade
    || null;
}

function memberPhotoEndpoint(member) {
  const email = memberEmail(member);
  const id = member?.id || member?.Id;
  if (!email && !id) return null;
  const params = new URLSearchParams();
  if (email) params.set('email', email);
  if (id) params.set('id', String(id));
  return `/api/equipa/photo?${params.toString()}`;
}

function memberGraphPhotoFromBatch(member, photoDataUrls) {
  if (photoDataUrls == null) return null;
  const email = memberEmail(member);
  if (!email) return null;
  if (photoDataUrls[email]) return photoDataUrls[email];
  const lower = String(email).toLowerCase();
  for (const [k, v] of Object.entries(photoDataUrls)) {
    if (String(k).toLowerCase() === lower && v) return v;
  }
  return null;
}

/** true when batch finished and Graph has no photo for this member; null while unknown/loading. */
function memberMissingMicrosoftPhoto(member, photoDataUrls) {
  if (photoDataUrls == null) return null;
  if (Object.keys(photoDataUrls).length === 0) return null;
  const email = memberEmail(member);
  if (!email) return null;
  const lower = String(email).toLowerCase();
  for (const [k, v] of Object.entries(photoDataUrls)) {
    if (String(k).toLowerCase() === lower) return !v;
  }
  return null;
}

/** Equipa avatars use Colaboradores Imagem only — same field as the list. */
function memberPhotoCandidates(member, photoDataUrls = undefined) {
  const sp = memberColaboradorImageUrl(member);
  return sp ? [sp] : [];
}

/** Instant img src map from Colaboradores Imagem. Graph batch is only for the missing-MS-photo mark. */
function buildEquipaInstantPhotoMap(items) {
  const map = {};
  for (const m of items || []) {
    const email = memberEmail(m);
    if (!email) continue;
    map[email] = memberColaboradorImageUrl(m);
  }
  return map;
}

function memberInitials(member) {
  const name = memberAssinatura(member);
  if (!name || name === '—') return '?';
  const cleaned = String(name).split('|')[0].trim();
  const parts = cleaned.split(/\s+/).filter(Boolean);
  if (parts.length === 0) return '?';
  if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
  return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase();
}

function MemberAvatar({ member, photoDataUrls = undefined, size, className = '' }) {
  const src = memberColaboradorImageUrl(member);
  const [failed, setFailed] = useState(false);

  useEffect(() => {
    setFailed(false);
  }, [src]);

  const sizeStyle = Number.isFinite(size) ? { width: size, height: size } : undefined;

  if (src && !failed) {
    return (
      <img
        src={src}
        alt=""
        className={`consultor-card-photo ${className}`.trim()}
        style={sizeStyle}
        loading="lazy"
        decoding="async"
        referrerPolicy="no-referrer"
        onError={() => setFailed(true)}
      />
    );
  }

  return (
    <span
      className={`consultor-card-initials ${className}`.trim()}
      style={Number.isFinite(size)
        ? { width: size, height: size, fontSize: Math.round(size * 0.32) }
        : undefined}
      aria-hidden="true"
    >
      {memberInitials(member)}
    </span>
  );
}

/** Location hub — large operação Foto + director avatar (Power Apps style) + count. */
function EquipaLocationNode({
  label,
  active,
  onClick,
  directorMember,
  directorTitle = null,
  imageUrl = null,
  count = 0,
  isAll = false,
  badgeTone = 0,
  renderMemberAvatar,
}) {
  const [imageFailed, setImageFailed] = useState(false);
  useEffect(() => { setImageFailed(false); }, [imageUrl]);

  const directorName = directorMember
    ? (memberAssinatura(directorMember) || directorMember.assinatura || directorMember.Assinatura || '')
    : '';
  const roleLabel = directorTitle
    || (isAll ? 'Diretor Geral' : 'Diretor(a)');
  const ariaParts = [label];
  if (directorName) ariaParts.push(`${roleLabel}: ${directorName}`);
  if (count) ariaParts.push(`${count} colaboradores`);
  const aria = ariaParts.join(' · ');
  const showFoto = imageUrl && !imageFailed;
  const fotoSrc = showFoto && imageUrl
    ? String(imageUrl).replace(/size=smallSquare/i, 'size=mediumSquare')
    : imageUrl;

  return (
    <button
      type="button"
      className={`equipa-location-node ${active ? 'active' : ''} ${isAll ? 'is-direcao' : ''}`}
      onClick={onClick}
      role="tab"
      aria-selected={active}
      aria-label={aria}
      title={aria}
    >
      <span className="equipa-location-media">
        <span className="equipa-location-color" aria-hidden="true" />
        <span className="equipa-location-clip" aria-hidden="true">
        {showFoto ? (
          <img
            src={fotoSrc}
            alt=""
            className="equipa-location-bg"
            loading="lazy"
            referrerPolicy="no-referrer"
            onError={() => setImageFailed(true)}
          />
        ) : (
          <span
            className={`equipa-location-bg-fallback ${isAll ? 'equipa-location-bg-fallback-direcao' : ''}`}
            aria-hidden="true"
          />
        )}
        </span>
        <span className={`equipa-location-count tone-${badgeTone % 3}`} aria-hidden="true">
          {count}
        </span>
        {directorMember ? (
          <span className="equipa-location-director" title={`${roleLabel}: ${directorName}`}>
            {renderMemberAvatar(directorMember, isAll ? 34 : 28)}
          </span>
        ) : null}
      </span>
      <span className="equipa-location-meta">
        <span className="equipa-location-label">{label}</span>
      </span>
    </button>
  );
}

function equipaMemberLocalLookupId(member) {
  const fields = member?.fields || member?.Fields || {};
  const raw = fields.LocalLookupId ?? fields.localLookupId;
  if (raw == null || raw === '') return null;
  const n = Number(raw);
  return Number.isFinite(n) ? n : null;
}

function equipaMemberMatchesLocal(member, localLabel, localDirectors) {
  if (!localLabel) return true;
  const loc = memberField(member, 'local', 'Local');
  if (loc && String(loc).toLowerCase() === String(localLabel).toLowerCase()) return true;
  const info = localDirectors[String(localLabel).trim().toLowerCase()];
  if (!info) return false;
  if (info.operacao && loc && String(loc).toLowerCase() === String(info.operacao).toLowerCase()) return true;
  if (info.local && loc && String(loc).toLowerCase() === String(info.local).toLowerCase()) return true;
  // Graph often returns only LocalLookupId — match against Operações list id.
  const lookupId = equipaMemberLocalLookupId(member);
  if (lookupId != null && info.id != null && Number(info.id) === lookupId) return true;
  return false;
}

function equipaResolveMemberLocalLabel(member, localDirectors) {
  const loc = memberField(member, 'local', 'Local');
  if (loc) return loc;
  const lookupId = equipaMemberLocalLookupId(member);
  if (lookupId == null) return null;
  for (const d of Object.values(localDirectors || {})) {
    if (d && d.id != null && Number(d.id) === lookupId) {
      return d.local || d.operacao || null;
    }
  }
  return null;
}

function FitOneLine({ className, children, minPx = 6 }) {
  const ref = useRef(null);
  const fit = useCallback(() => {
    const el = ref.current;
    if (!el) return;
    el.style.fontSize = '';
    const base = parseFloat(getComputedStyle(el).fontSize);
    if (!base || el.clientWidth < 4) return;
    let size = base;
    while (el.scrollWidth > el.clientWidth + 0.5 && size > minPx) {
      size -= 0.4;
      el.style.fontSize = `${size}px`;
    }
  }, [minPx, children]);
  useLayoutEffect(() => {
    fit();
    const el = ref.current;
    if (!el || typeof ResizeObserver === 'undefined') return undefined;
    const ro = new ResizeObserver(fit);
    ro.observe(el);
    return () => ro.disconnect();
  }, [fit]);
  return <span ref={ref} className={className}>{children}</span>;
}

function EquipaMemberCard({
  member,
  selected,
  allowViewAs,
  allowManage = false,
  showMissingMsPhotoMark = false,
  onSelect,
  onManage = null,
  onEnterAs,
  renderMemberAvatar,
  enteringAs,
  index = 0,
  localDirectors = {},
  commissionRank = 0,
  topCommissionLabel = '',
  antiguidadeRank = 0,
  topAntiguidadeLabel = '',
}) {
  const [perfilFailed, setPerfilFailed] = useState(false);
  const assinatura = memberAssinatura(member);
  const funcao = memberField(member, 'funcao', 'Funcao');
  const telefone = memberField(member, 'telefone', 'Telefone');
  const email = memberEmail(member);
  const local = equipaResolveMemberLocalLabel(member, localDirectors);
  const antiguidade = memberAntiguidadeLabel(member);
  const flipped = Boolean(selected);
  const perfilSrc = flipped ? memberImagemPerfilUrl(member, 'large') : null;
  const showPerfil = Boolean(perfilSrc && !perfilFailed);
  useEffect(() => {
    setPerfilFailed(false);
  }, [member?.id, member?.Id, member?.imagemPerfilVersion, member?.ImagemPerfilVersion]);
  const isTopCommission = commissionRank >= 1 && commissionRank <= 3;
  const isTopAntiguidade = antiguidadeRank >= 1 && antiguidadeRank <= 3;
  const starCount = isTopCommission ? (4 - commissionRank) : 0;

  const ariaBits = [assinatura];
  if (isTopCommission) ariaBits.push(`${commissionRank}º em comissão`);
  if (isTopAntiguidade) ariaBits.push(`${antiguidadeRank}º em antiguidade`);
  if (flipped) ariaBits.push('detalhes');

  return (
    <div
      className={[
        'equipa-member-card',
        flipped ? 'selected is-flipped' : '',
        (allowViewAs || allowManage) ? 'selectable' : '',
        isTopCommission ? `is-top-commission rank-${commissionRank}` : '',
        isTopAntiguidade ? `is-top-antiguidade rank-${antiguidadeRank}` : '',
      ].filter(Boolean).join(' ')}
      style={{ animationDelay: `${Math.min(index * 35, 420)}ms` }}
      onClick={() => onSelect(member)}
      role="button"
      tabIndex={0}
      aria-pressed={flipped}
      aria-label={ariaBits.join(' — ')}
      onKeyDown={(e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          onSelect(member);
        }
      }}
    >
      <div className="equipa-member-card-inner">
        <div className="equipa-member-card-face equipa-member-card-front">
          <div className={`equipa-member-card-avatar ${isTopCommission ? 'has-star' : ''}${showMissingMsPhotoMark ? ' has-ms-photo-mark' : ''}${isTopAntiguidade ? ' has-antiguidade' : ''}`}>
            <span className="equipa-member-card-avatar-ring">
              {renderMemberAvatar(member)}
            </span>
            {showMissingMsPhotoMark ? (
              <span
                className="equipa-member-card-ms-photo-mark"
                title="Sem foto na conta Microsoft"
                aria-label="Sem foto na conta Microsoft"
              >
                <Icon name="camera" size={10} color="#FFFFFF" />
              </span>
            ) : null}
            {isTopAntiguidade ? (
              <span
                className={`equipa-member-card-antiguidade rank-${antiguidadeRank}`}
                title={topAntiguidadeLabel || `${antiguidadeRank}º em antiguidade`}
                aria-label={topAntiguidadeLabel || `${antiguidadeRank}º em antiguidade`}
              >
                <Icon name="calendar" size={10} color="#FFFFFF" />
              </span>
            ) : null}
            {isTopCommission ? (
              <span
                className={`equipa-member-card-star rank-${commissionRank}`}
                title={topCommissionLabel || `${commissionRank}º em comissão aprovada`}
                aria-label={topCommissionLabel || `${commissionRank}º em comissão aprovada`}
              >
                {Array.from({ length: starCount }, (_, i) => (
                  <Icon key={i} name="star" size={starCount > 1 ? 9 : 11} color="#FFFFFF" />
                ))}
              </span>
            ) : null}
          </div>
          <div className="equipa-member-card-name">{assinatura}</div>
          <div className="equipa-member-card-role">{funcao || '—'}</div>
          {local ? <div className="equipa-member-card-meta">{local}</div> : null}
          <div className="equipa-member-card-actions" onClick={(e) => e.stopPropagation()}>
            {telefone ? (
              <a
                className="equipa-member-row-ico"
                href={`tel:${String(telefone).replace(/\s+/g, '')}`}
                title={telefone}
                aria-label={`Ligar ${telefone}`}
              >
                <Icon name="phone" size={12} />
              </a>
            ) : null}
            {email ? (
              <a
                className="equipa-member-row-ico"
                href={`mailto:${email}`}
                title={email}
                aria-label={`Email ${email}`}
              >
                <Icon name="envelope" size={12} />
              </a>
            ) : null}
          </div>
        </div>

        <div className={`equipa-member-card-face equipa-member-card-back${showPerfil ? ' has-perfil' : ''}`}>
          <div className="equipa-member-card-back-copy">
            <span className="equipa-member-card-back-name">{assinatura}</span>
            <span className="equipa-member-card-back-role">{funcao || '—'}</span>
            <div className="equipa-member-card-back-fields">
              {email ? (
                <a className="equipa-member-card-back-row" href={`mailto:${email}`} onClick={(e) => e.stopPropagation()}>
                  <span className="equipa-member-card-back-ico"><Icon name="envelope" size={11} /></span>
                  <span className="equipa-member-card-back-value">{email}</span>
                </a>
              ) : null}
              {telefone ? (
                <a className="equipa-member-card-back-row" href={`tel:${String(telefone).replace(/\s+/g, '')}`} onClick={(e) => e.stopPropagation()}>
                  <span className="equipa-member-card-back-ico"><Icon name="phone" size={11} /></span>
                  <span className="equipa-member-card-back-value">{telefone}</span>
                </a>
              ) : null}
              {antiguidade ? (
                <AppTooltip label={`${antiguidade} na HABITA`} className="equipa-member-card-back-tip">
                  <div className="equipa-member-card-back-row">
                    <span className="equipa-member-card-back-ico"><Icon name="calendar" size={11} /></span>
                    <span className="equipa-member-card-back-value">{antiguidade}</span>
                  </div>
                </AppTooltip>
              ) : null}
              {local ? (
                <div className="equipa-member-card-back-row">
                  <span className="equipa-member-card-back-ico"><Icon name="location" size={11} /></span>
                  <span className="equipa-member-card-back-value">{local}</span>
                </div>
              ) : null}
            </div>
            <div className="equipa-member-card-back-actions" onClick={(e) => e.stopPropagation()}>
              {allowViewAs && email ? (
                <button
                  type="button"
                  className="equipa-member-row-ico on-dark"
                  title="Entrar como"
                  aria-label={`Entrar como ${assinatura}`}
                  disabled={enteringAs}
                  onClick={() => onEnterAs(member)}
                >
                  <Icon name="eye" size={12} />
                </button>
              ) : null}
              {allowManage && onManage ? (
                <button
                  type="button"
                  className="equipa-member-row-ico on-dark"
                  title="Opções"
                  aria-label={`Opções de ${assinatura}`}
                  onClick={() => onManage(member)}
                >
                  <Icon name="settings" size={12} />
                </button>
              ) : null}
            </div>
          </div>
          {showPerfil ? (
            <img
              src={perfilSrc}
              alt=""
              className="equipa-member-card-perfil-img"
              aria-hidden="true"
              draggable="false"
              decoding="async"
              onError={() => setPerfilFailed(true)}
            />
          ) : null}
        </div>
      </div>
    </div>
  );
}

const EQUIPA_LIST_FALLBACK_COLUMNS = [
  { key: 'imagem', label: 'Imagem', visible: true, order: 0, width: 72, filterable: false, sortable: false },
  { key: 'assinatura', label: 'Assinatura', visible: true, order: 1 },
  { key: 'funcao', label: 'Função', visible: true, order: 2 },
  { key: 'local', label: 'Local', visible: true, order: 3 },
  { key: 'telefone', label: 'Telefone', visible: true, order: 4 },
  { key: 'antiguidade', label: 'Antiguidade', visible: true, order: 5 },
  { key: 'estado', label: 'Estado', visible: true, order: 6, filterType: 'choice', choices: ['Sim', 'Não', 'Ativo', 'Inativo'] },
];

function equipaMemberToGridRow(member) {
  const fields = member.fields || member.Fields || {};
  const assinatura = memberAssinatura(member);
  const funcao = memberField(member, 'funcao', 'Funcao');
  const local = memberField(member, 'local', 'Local');
  const telefone = memberField(member, 'telefone', 'Telefone');
  const antiguidade = memberField(member, 'antiguidade', 'Antiguidade');
  const estado = memberField(member, 'estado', 'Estado');
  return {
    ...fields,
    id: member.id || member.Id,
    Id: member.id || member.Id,
    assinatura,
    Assinatura: assinatura,
    funcao: funcao || '',
    Funcao: funcao || '',
    local: local || '',
    Local: local || '',
    telefone: telefone || '',
    Telefone: telefone || '',
    antiguidade: antiguidade || '',
    Antiguidade: antiguidade || '',
    estado: estado || '',
    Estado: estado || '',
    email: memberEmail(member),
    Email: memberEmail(member),
    _member: member,
  };
}

function EquipaPage({ user, equipaEnabled, canViewAs, onEnterAs, activeTab, onTabChange, onConfigureListColumns = null }) {
  const page = normalizeRouteTab('equipa', activeTab) || 'main';
  const canRecrutamentoCmi = userCanAccessRecrutamentoCmi(user);
  const [listView, setListView] = useEquipaListView();
  const equipaListView = userIsAdmin(user) ? listView : 'cards';
  const [extReady, setExtReady] = useState(() => Boolean(
    window.EquipaRecrutamentoPanel && window.EquipaCmiPanel && window.EquipaColaboradorOptions
  ));
  const [search, setSearch] = useState('');
  const [estado, setEstado] = useState('');
  const [selectedLocal, setSelectedLocal] = useState('');
  const [members, setMembers] = useState([]);
  const [stats, setStats] = useState({ total: 0, consultores: 0 });
  const [equipaColumns, setEquipaColumns] = useState(EQUIPA_LIST_FALLBACK_COLUMNS);
  const [optionsMember, setOptionsMember] = useState(null);
  const canFilterEstado = Boolean(
    user?.isAdmin || user?.IsAdmin ||
    user?.canUpdate || user?.CanUpdate ||
    user?.canDelete || user?.CanDelete
  );
  const canManageColaborador = Boolean(
    userIsAdmin(user) || user?.canUpdate || user?.CanUpdate
  );
  const isEquipaAdmin = userIsAdmin(user);

  useEffect(() => {
    if (extReady) return undefined;
    const mark = () => {
      if (window.EquipaRecrutamentoPanel && window.EquipaCmiPanel && window.EquipaColaboradorOptions) {
        setExtReady(true);
        return true;
      }
      return false;
    };
    if (mark()) return undefined;
    const onReady = () => { mark(); };
    window.addEventListener('myhabita-equipa-ext-ready', onReady);
    const timer = setInterval(() => { if (mark()) clearInterval(timer); }, 80);
    return () => {
      window.removeEventListener('myhabita-equipa-ext-ready', onReady);
      clearInterval(timer);
    };
  }, [extReady]);

  useEffect(() => {
    if (!canRecrutamentoCmi && (page === 'recrutamento' || page === 'comunicacaointerna')) {
      onTabChange('main');
    }
  }, [canRecrutamentoCmi, page, onTabChange]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [viewAsOpen, setViewAsOpen] = useState(false);
  const [enteringAs, setEnteringAs] = useState(false);
  const [selectedMember, setSelectedMember] = useState(null);
  /** @type {[Record<string, {email?: string, assinatura?: string, imageUrl?: string, isHeadquarters?: boolean, id?: number}>, Function]} */
  const [localDirectors, setLocalDirectors] = useState({});
  const [direcaoInfo, setDirecaoInfo] = useState(null);
  const [photoDataUrls, setPhotoDataUrls] = useState(null);
  /** @type {[{ email: string|null, name: string|null, sum: number, year?: number }[], Function]} */
  const [topCommissions, setTopCommissions] = useState([]);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      const { defs } = await fetchListGridColumns('himo-colaboradores', EQUIPA_LIST_FALLBACK_COLUMNS);
      if (!cancelled && defs?.length) {
        // Always keep avatar column first when settings omit it.
        const hasImagem = defs.some(d => String(d.key).toLowerCase() === 'imagem');
        setEquipaColumns(hasImagem ? defs : [
          EQUIPA_LIST_FALLBACK_COLUMNS[0],
          ...defs.map((d, i) => ({ ...d, order: (d.order ?? i) + 1 })),
        ]);
      }
    })();
    return () => { cancelled = true; };
  }, []);

  useEffect(() => {
    if (!equipaEnabled) return;
    let cancelled = false;
    (async () => {
      try {
        const result = await apiFetch('/api/equipa/local-directors');
        if (cancelled) return;
        const map = {};
        let hq = null;
        for (const item of result.items || result.Items || []) {
          const operacao = item.operacao || item.Operacao;
          if (!operacao) continue;
          const local = item.local || item.Local || null;
          const entry = {
            id: item.id ?? item.Id,
            email: item.directorEmail || item.DirectorEmail || null,
            assinatura: item.directorName || item.DirectorName || operacao,
            imageUrl: item.imageUrl || item.ImageUrl || null,
            isHeadquarters: Boolean(item.isHeadquarters ?? item.IsHeadquarters),
            local,
            operacao,
          };
          // Rail nodes use Colaboradores.Local (e.g. Sines); Operações.Title may differ (Alentejo).
          map[String(operacao).trim().toLowerCase()] = entry;
          if (local) map[String(local).trim().toLowerCase()] = entry;
          if (entry.isHeadquarters && !hq) hq = entry;
        }
        setLocalDirectors(map);
        setDirecaoInfo(hq);
      } catch {
        if (!cancelled) {
          setLocalDirectors({});
          setDirecaoInfo(null);
        }
      }
    })();
    return () => { cancelled = true; };
  }, [equipaEnabled]);

  // Top 3 = approved commission via dedicated Equipa API (stars). Do not block the grid on this.
  useEffect(() => {
    if (!equipaEnabled) {
      setTopCommissions([]);
      return;
    }
    let cancelled = false;
    const year = new Date().getFullYear();
    (async () => {
      try {
        const result = await apiFetch(`/api/equipa/top-comissao?ano=${encodeURIComponent(year)}&take=3`);
        if (cancelled) return;
        const items = (result.items || result.Items || []).map((row, i) => ({
          email: (row.email || row.Email || null) ? String(row.email || row.Email).trim().toLowerCase() : null,
          name: (row.name || row.Name || null) ? String(row.name || row.Name).trim() : null,
          sum: Number(row.sum ?? row.Sum ?? 0) || 0,
          year: Number(row.year ?? row.Year ?? year) || year,
          rank: Number(row.rank ?? row.Rank ?? i + 1) || i + 1,
        })).filter(r => r.sum > 0);
        setTopCommissions(items.slice(0, 3));
      } catch {
        if (!cancelled) setTopCommissions([]);
      }
    })();
    return () => { cancelled = true; };
  }, [equipaEnabled]);

  useEffect(() => {
    if (!equipaEnabled) {
      setLoading(false);
      return;
    }
    let cancelled = false;
    const timer = setTimeout(async () => {
      setLoading(true);
      setError(null);
      try {
        const params = new URLSearchParams();
        if (search) params.set('search', search);
        if (canFilterEstado && estado) params.set('estado', estado);
        const result = await apiFetch(`/api/equipa/colaboradores?${params}`);
        if (!cancelled) {
          const items = result.items || result.Items || [];
          setMembers(items);
          setStats({
            total: result.total ?? result.Total ?? items.length,
            consultores: result.consultores ?? result.Consultores ?? 0,
          });
          // Instant: local SP /photo URLs — do not wait for Graph batch.
          setPhotoDataUrls(buildEquipaInstantPhotoMap(items));
          const emails = items
            .map(m => memberEmail(m))
            .filter(Boolean)
            .slice(0, 80);
          if (emails.length) {
            // Background warm only (batch returns cached /photo URLs, fills Graph disk cache).
            apiFetch('/api/equipa/photos/batch', {
              method: 'POST',
              body: JSON.stringify({ items: emails.map(email => ({ email })) }),
            }).then(photos => {
              if (cancelled || !photos) return;
              setPhotoDataUrls(prev => {
                const next = { ...(prev || {}) };
                for (const [k, v] of Object.entries(photos)) {
                  if (v) next[k] = v;
                }
                return next;
              });
            }).catch(() => { /* keep instant map */ });
          }
        }
      } catch (err) {
        if (!cancelled) setError({ message: err.message, code: err.code });
      } finally {
        if (!cancelled) setLoading(false);
      }
    }, search ? 300 : 0);
    return () => { cancelled = true; clearTimeout(timer); };
  }, [equipaEnabled, search, estado, canFilterEstado]);

  const allLocations = useMemo(() => {
    // Rail nodes come only from active Operações (API already filters Ativa/Ativo = Sim).
    const unique = new Map();
    for (const d of Object.values(localDirectors)) {
      if (!d || d.isHeadquarters) continue;
      const label = String(d.local || d.operacao || '').trim();
      if (!label) continue;
      const key = d.id != null ? `id:${d.id}` : label.toLowerCase();
      if (!unique.has(key)) unique.set(key, label);
    }
    return [...unique.values()].sort((a, b) => a.localeCompare(b, 'pt'));
  }, [localDirectors]);

  useEffect(() => {
    if (!selectedLocal) return;
    const stillVisible = allLocations.some(
      loc => String(loc).toLowerCase() === String(selectedLocal).toLowerCase()
    );
    if (!stillVisible) setSelectedLocal('');
  }, [allLocations, selectedLocal]);

  const normalizePersonKey = (value) => String(value || '')
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '');

  const memberCommissionRank = (member) => {
    if (!topCommissions.length) return 0;
    if (equipaIsDiretorGeral(memberField(member, 'funcao', 'Funcao'))) return 0;
    if (equipaIsCanalHabita(member)) return 0;
    const email = String(memberEmail(member) || '').trim().toLowerCase();
    const nameKey = normalizePersonKey(memberAssinatura(member));
    for (let i = 0; i < topCommissions.length; i += 1) {
      const top = topCommissions[i];
      if (top.email && email && top.email === email) return top.rank || (i + 1);
      const topNameKey = normalizePersonKey(top.name);
      if (topNameKey && nameKey && topNameKey === nameKey) return top.rank || (i + 1);
    }
    return 0;
  };

  /** Top 3 longest tenure (earliest admission date). */
  const topAntiguidade = useMemo(() => {
    return [...members]
      .map((m) => {
        const date = memberAdmissionDate(m);
        if (!date) return null;
        return {
          id: String(m.id || m.Id),
          email: String(memberEmail(m) || '').trim().toLowerCase(),
          name: memberAssinatura(m),
          date,
          label: memberAntiguidadeLabel(m) || '',
        };
      })
      .filter(Boolean)
      .sort((a, b) => a.date - b.date)
      .slice(0, 3)
      .map((row, i) => ({ ...row, rank: i + 1 }));
  }, [members]);

  const memberAntiguidadeRank = (member) => {
    if (!topAntiguidade.length) return 0;
    const id = String(member?.id || member?.Id || '');
    const email = String(memberEmail(member) || '').trim().toLowerCase();
    for (const row of topAntiguidade) {
      if (id && row.id === id) return row.rank;
      if (email && row.email && row.email === email) return row.rank;
    }
    return 0;
  };

  const sortedMembers = useMemo(
    () => [...members].sort((a, b) => compareEquipaMembers(a, b, memberCommissionRank)),
    [members, topCommissions]
  );

  const displayedMembers = (selectedLocal
    ? sortedMembers.filter(m => equipaMemberMatchesLocal(m, selectedLocal, localDirectors))
    : sortedMembers);

  const locationCounts = useMemo(() => {
    const counts = {};
    for (const loc of allLocations) {
      counts[loc] = sortedMembers.filter(m => equipaMemberMatchesLocal(m, loc, localDirectors)).length;
    }
    return counts;
  }, [allLocations, sortedMembers, localDirectors]);

  const gridRows = useMemo(
    () => displayedMembers.map(equipaMemberToGridRow),
    [displayedMembers]
  );

  const allowViewAs = Boolean(canViewAs);

  const displayedStats = {
    total: displayedMembers.length,
    consultores: displayedMembers.filter(m => {
      const f = memberField(m, 'funcao', 'Funcao') || '';
      return f.toLowerCase().includes('consultor');
    }).length,
  };

  const memberTopCommissionLabel = (member) => {
    const rank = memberCommissionRank(member);
    if (!rank) return '';
    const top = topCommissions[rank - 1];
    if (!top) return '';
    const place = rank === 1 ? '1º' : rank === 2 ? '2º' : '3º';
    return `${place} em comissão aprovada ${top.year || new Date().getFullYear()}: ${formatCurrency(top.sum)}`;
  };

  const memberTopAntiguidadeLabel = (member) => {
    const rank = memberAntiguidadeRank(member);
    if (!rank) return '';
    const top = topAntiguidade[rank - 1];
    if (!top) return '';
    const place = rank === 1 ? '1º' : rank === 2 ? '2º' : '3º';
    const years = top.label || memberAntiguidadeLabel(member) || '';
    return `${place} em antiguidade${years ? `: ${years}` : ''}`;
  };

  const toggleMember = (member) => {
    const id = member.id || member.Id;
    const selectedId = selectedMember?.id || selectedMember?.Id;
    setSelectedMember(selectedId === id ? null : member);
  };

  const handleEnterAs = async (member) => {
    const email = memberEmail(member) || member?.email || member?.Email;
    if (!email || !onEnterAs) return;
    setEnteringAs(true);
    setOptionsMember(null);
    try {
      await onEnterAs({ ...member, email });
      setViewAsOpen(false);
      setSelectedMember(null);
    } catch (err) {
      toast.error(err.message || 'Não foi possível entrar como este utilizador.');
    } finally {
      setEnteringAs(false);
    }
  };

  const renderMemberAvatar = (member, size) => (
    <MemberAvatar member={member} photoDataUrls={photoDataUrls} size={size} />
  );

  const diretorGeralFallback = useMemo(() => {
    // Prefer the actual Diretor Geral (e.g. Joana Gomes) as head of the rail.
    const byFuncao = members.find(m => {
      const f = String(memberField(m, 'funcao', 'Funcao') || '');
      return /diretor\s*geral/i.test(f);
    });
    if (byFuncao) return byFuncao;
    if (direcaoInfo?.email) {
      return { email: direcaoInfo.email, assinatura: direcaoInfo.assinatura || 'Diretor Geral' };
    }
    return null;
  }, [direcaoInfo, members]);

  const pageTitle = page === 'recrutamento'
    ? 'Recrutamentos'
    : page === 'comunicacaointerna'
      ? 'Comunicação Interna'
      : 'Equipa';

  const tabsSlot = (
    <div className="tabs tabs-segmented module-tabs" role="tablist" aria-label="Secções Equipa">
      <button type="button" className={`tab ${page === 'main' ? 'active' : ''}`} role="tab" aria-selected={page === 'main'} onClick={() => onTabChange('main')}>Equipa</button>
      {canRecrutamentoCmi && (
        <button type="button" className={`tab ${page === 'comunicacaointerna' ? 'active' : ''}`} role="tab" aria-selected={page === 'comunicacaointerna'} onClick={() => onTabChange('comunicacaointerna')}>Comunicação Interna</button>
      )}
      {canRecrutamentoCmi && (
        <button type="button" className={`tab ${page === 'recrutamento' ? 'active' : ''}`} role="tab" aria-selected={page === 'recrutamento'} onClick={() => onTabChange('recrutamento')}>Recrutamentos</button>
      )}
    </div>
  );

  const reloadMembers = useCallback((detail) => {
    setLoading(true);
    const qs = new URLSearchParams();
    if (search) qs.set('search', search);
    if (estado) qs.set('estado', estado);
    apiFetch(`/api/equipa/colaboradores?${qs}`)
      .then((result) => {
        const items = result.items || result.Items || [];
        setMembers(items);
        setStats({
          total: result.total ?? result.Total ?? 0,
          consultores: result.consultores ?? result.Consultores ?? 0,
        });
        const emails = items.map(m => memberEmail(m)).filter(Boolean).slice(0, 80);
        setPhotoDataUrls(buildEquipaInstantPhotoMap(items));
        if (!emails.length) return;
        return apiFetch('/api/equipa/photos/batch', {
          method: 'POST',
          body: JSON.stringify({ items: emails.map(email => ({ email })) }),
        }).then((photos) => {
          if (photos) {
            setPhotoDataUrls(prev => {
              const next = { ...(prev || {}) };
              for (const [k, v] of Object.entries(photos)) {
                if (v) next[k] = v;
              }
              return next;
            });
          }
          // After syncing one photo, Graph CDN can lag — force a single-email refresh shortly.
          if (detail?.syncedPhoto && detail.email) {
            window.setTimeout(() => {
              apiFetch('/api/equipa/photos/batch', {
                method: 'POST',
                body: JSON.stringify({ items: [{ email: detail.email }] }),
              }).then((again) => {
                if (!again) return;
                setPhotoDataUrls(prev => {
                  const next = { ...(prev || {}) };
                  for (const [k, v] of Object.entries(again)) {
                    if (v) next[k] = v;
                  }
                  return next;
                });
              }).catch(() => { /* ignore */ });
            }, 1500);
          }
        }).catch(() => { /* keep instant map */ });
      })
      .catch((err) => setError({ message: err.message, code: err.code }))
      .finally(() => setLoading(false));
  }, [search, estado]);

  const toolbarSlot = page === 'main' ? (
    <div className="toolbar equipa-shell-toolbar">
      <div className="toolbar-filters">
        {canFilterEstado && (
          <AppSelect
            variant="toolbar"
            value={estado}
            emptyOption="Activos"
            options={[
              { value: 'Sim', label: 'Sim' },
              { value: 'Admissão', label: 'Admissão' },
              { value: 'Transferência', label: 'Transferência' },
              { value: 'Saida', label: 'Saída' },
              { value: 'Não', label: 'Não' },
            ]}
            onChange={setEstado}
            ariaLabel="Estado"
          />
        )}
        <input
          placeholder="Procurar colaborador"
          value={search}
          onChange={e => setSearch(e.target.value)}
        />
        <div className="toolbar-spacer" />
        <div className="equipa-stats-inline">
          <span>{displayedStats.total} colaboradores</span>
          <span className="equipa-stats-dot" aria-hidden="true" />
          <span>{displayedStats.consultores} consultores</span>
        </div>
      </div>
      <div className="toolbar-actions">
        <button
          type="button"
          className="btn btn-secondary toolbar-icon-btn"
          onClick={() => reloadMembers()}
          disabled={loading}
          title="Atualizar"
          aria-label="Atualizar"
        >
          <Icon name="refresh" size={16} />
        </button>
        {userIsAdmin(user) && (
          <EquipaListViewToggle view={listView} onChange={setListView} />
        )}
        {canManageColaborador && selectedMember ? (
          <button
            type="button"
            className="btn btn-secondary btn-sm"
            onClick={() => setOptionsMember(selectedMember)}
            title="Opções do colaborador"
          >
            Opções
          </button>
        ) : null}
        {equipaListView === 'lista' && (
          <MapearColunasToolbarButton
            user={user}
            listKey="himo-colaboradores"
            onConfigureListColumns={onConfigureListColumns}
          />
        )}
      </div>
    </div>
  ) : null;

  const RecrutamentoPanel = window.EquipaRecrutamentoPanel;
  const CmiPanel = window.EquipaCmiPanel;
  const ColaboradorOptions = window.EquipaColaboradorOptions;

  return (
    <>
      <ModuleTopbar
        icon="team"
        title={pageTitle}
        user={user}
        actions={allowViewAs && page === 'main' ? (
          <button type="button" className="btn-icon-ghost equipa-enteras-btn" onClick={() => setViewAsOpen(true)} title="Entrar como" aria-label="Entrar como">
            <Icon name="eye" size={18} />
          </button>
        ) : null}
      />
      <div className="page-body">
        <div className="page-content module-layout equipa-layout">
          <ModulePageShell tabs={tabsSlot} toolbar={toolbarSlot}>
            {page === 'recrutamento' && (
              extReady && RecrutamentoPanel
                ? <RecrutamentoPanel user={user} onConfigureListColumns={onConfigureListColumns} />
                : <LoadingSpinner message="A carregar Recrutamentos…" />
            )}
            {page === 'comunicacaointerna' && (
              extReady && CmiPanel
                ? <CmiPanel user={user} />
                : <LoadingSpinner message="A carregar Comunicação Interna…" />
            )}
            {page === 'main' && allLocations.length > 0 && (
              <div className="equipa-location-rail" role="tablist" aria-label="Localizações">
                <div className="equipa-location-orbit">
                <EquipaLocationNode
                  label="Direção"
                  active={!selectedLocal}
                  onClick={() => setSelectedLocal('')}
                  directorMember={diretorGeralFallback}
                  directorTitle="Diretor Geral"
                  imageUrl="/images/logo-habita-dark.svg"
                  count={sortedMembers.length}
                  isAll
                  badgeTone={0}
                  renderMemberAvatar={renderMemberAvatar}
                />
                <div className="equipa-location-rail-locals">
                  {allLocations.map((loc, i) => {
                    const info = localDirectors[String(loc).trim().toLowerCase()];
                    if (info?.isHeadquarters) return null;
                    const email = info?.email || null;
                    const fromMembers = email
                      ? members.find(m => String(memberEmail(m) || '').toLowerCase() === String(email).toLowerCase())
                      : null;
                    const directorMember = fromMembers
                      || (email ? { email, assinatura: info.assinatura || loc } : null);
                    return (
                      <EquipaLocationNode
                        key={loc}
                        label={String(loc)}
                        active={selectedLocal === loc}
                        onClick={() => setSelectedLocal(selectedLocal === loc ? '' : loc)}
                        directorMember={directorMember}
                        directorTitle="Diretor(a)"
                        imageUrl={info?.imageUrl || null}
                        count={locationCounts[loc] || 0}
                        badgeTone={i + 1}
                        renderMemberAvatar={renderMemberAvatar}
                      />
                    );
                  })}
                </div>
                </div>
              </div>
            )}

            {page === 'main' && loading && members.length === 0 && <LoadingSpinner message="A carregar equipa HABITA (M2)..." />}
            {page === 'main' && error && <ErrorBanner error={error} onDismiss={() => setError(null)} />}

            {page === 'main' && !error && equipaListView === 'cards' && !(loading && members.length === 0) && (
              <div className="equipa-cards">
                {displayedMembers.length === 0 ? (
                  <p className="empty-hint">Nenhum colaborador HABITA (M2) encontrado no HIMO Colaboradores.</p>
                ) : (
                  displayedMembers.map((member, index) => {
                    const id = member.id || member.Id;
                    const selectedId = selectedMember?.id || selectedMember?.Id;
                    return (
                      <EquipaMemberCard
                        key={id}
                        member={member}
                        selected={selectedId === id}
                        allowViewAs={allowViewAs}
                        allowManage={canManageColaborador}
                        showMissingMsPhotoMark={
                          isEquipaAdmin && memberMissingMicrosoftPhoto(member, photoDataUrls) === true
                        }
                        onSelect={toggleMember}
                        onManage={(m) => setOptionsMember(m)}
                        onEnterAs={handleEnterAs}
                        renderMemberAvatar={renderMemberAvatar}
                        enteringAs={enteringAs}
                        index={index}
                        localDirectors={localDirectors}
                        commissionRank={memberCommissionRank(member)}
                        topCommissionLabel={memberTopCommissionLabel(member)}
                        antiguidadeRank={memberAntiguidadeRank(member)}
                        topAntiguidadeLabel={memberTopAntiguidadeLabel(member)}
                      />
                    );
                  })
                )}
              </div>
            )}

            {page === 'main' && !error && equipaListView === 'lista' && !(loading && members.length === 0) && (
              <div className="module-page-table">
                <DataGrid
                  storageKey="equipa-colaboradores"
                  columns={equipaColumns}
                  rows={gridRows}
                  selectedId={selectedMember?.id || selectedMember?.Id || null}
                  onRowClick={(row) => toggleMember(row._member || row)}
                  emptyMessage="Nenhum colaborador HABITA (M2) encontrado no HIMO Colaboradores."
                  renderCell={(row, col) => {
                    const key = String(col.key || '').toLowerCase();
                    const member = row._member || row;
                    if (key === 'imagem' || key === 'photo' || key === 'foto') {
                      return (
                        <div className="equipa-table-avatar">{renderMemberAvatar(member, 40)}</div>
                      );
                    }
                    return undefined;
                  }}
                />
              </div>
            )}
          </ModulePageShell>
        </div>
      </div>
      <ViewAsPickerModal
        open={viewAsOpen}
        members={sortedMembers}
        loading={loading}
        onClose={() => setViewAsOpen(false)}
        onConfirm={handleEnterAs}
        confirming={enteringAs}
      />
      {ColaboradorOptions ? (
        <ColaboradorOptions
          member={optionsMember}
          user={user}
          open={Boolean(optionsMember)}
          onClose={() => setOptionsMember(null)}
          onEdited={reloadMembers}
          canViewAs={allowViewAs}
          onEnterAs={handleEnterAs}
        />
      ) : null}
    </>
  );
}

const MEDIACAO_NEGOCIOS_FALLBACK_COLUMNS = [
  { key: 'operacao', label: 'Operação', visible: true, order: 0 },
  { key: 'consultor', label: 'Consultor', visible: true, order: 1, fill: true },
  {
    key: 'estado',
    label: 'Estado',
    visible: true,
    order: 2,
    filterType: 'choice',
    choices: ['Em Curso', 'Concluído', 'Perdido'],
  },
  {
    key: 'probabilidade',
    label: 'Prob.',
    visible: true,
    order: 3,
    filterType: 'choice',
    choices: ['Alta', 'Média'],
  },
  {
    key: 'naturezaNegocio',
    label: 'Natureza',
    visible: true,
    order: 4,
    filterType: 'choice',
    choices: ['Venda', 'Arrendamento'],
  },
  { key: 'comissaoTotalHabita', label: 'Comissão', visible: true, order: 5 },
  { key: 'data', label: 'Data', visible: true, order: 6 },
  { key: 'dataPrevistaFaturacao', label: 'Data Prev.', visible: true, order: 7 },
  { key: 'referencia', label: 'Referência', visible: true, order: 8 },
  { key: 'montante', label: 'Montante', visible: true, order: 9 },
];

function pickGridEqualsFilter(gridFilters, key) {
  const f = gridFilters?.[key];
  if (!f || !columnFilterIsActive(f)) return '';
  if ((f.op1 === 'equals' || f.op1 === 'contains') && String(f.val1 || '').trim()) {
    return String(f.val1).trim();
  }
  return '';
}

function pickGridEqualsFilterFor(gridFilters, columns, ...aliases) {
  const wanted = new Set(aliases.map(a => String(a).toLowerCase()));
  for (const col of columns || []) {
    const keys = [col.key, col.aliasKey].filter(Boolean);
    if (!keys.some(k => wanted.has(String(k).toLowerCase()))) continue;
    const v = pickGridEqualsFilter(gridFilters, col.key);
    if (v) return v;
  }
  for (const a of aliases) {
    const v = pickGridEqualsFilter(gridFilters, a);
    if (v) return v;
  }
  return '';
}

function attachMediacaoChoiceOptions(defs, spColumns) {
  const byNegocioKey = new Map();
  (spColumns || []).forEach(col => {
    const name = col.name || col.Name;
    if (!name) return;
    const negocioKey = mapPipelineFieldToNegocioKey(name);
    const choices = col.choices || col.Choices;
    if (Array.isArray(choices) && choices.length > 0) {
      byNegocioKey.set(negocioKey, choices.map(c => String(c)));
    }
  });

  return (defs || []).map(col => {
    const fromSp = byNegocioKey.get(col.key)
      || (col.aliasKey ? byNegocioKey.get(col.aliasKey) : null)
      || byNegocioKey.get(mapPipelineFieldToNegocioKey(col.key));
    const choices = fromSp && fromSp.length ? fromSp : (col.choices || null);
    if (!choices || !choices.length) return col;
    return {
      ...col,
      filterType: col.filterType || 'choice',
      choices,
    };
  });
}

/** Map SharePoint Pipeline field names → NegocioDto property keys used by /api/mediacao. */
function mapPipelineFieldToNegocioKey(fieldKey) {
  const key = String(fieldKey || '');
  const lower = key.toLowerCase();
  const exact = {
    operacao: 'operacao',
    consultor: 'consultor',
    consultoremail: 'consultorEmail',
    estado: 'estado',
    probabilidade: 'probabilidade',
    naturezanegocio: 'naturezaNegocio',
    comissaototalhabita: 'comissaoTotalHabita',
    data: 'data',
    dataprevistafaturacao: 'dataPrevistaFaturacao',
    dataprevistarecebimento: 'dataPrevistaRecebimento',
    referencia: 'referencia',
    montante: 'montante',
    title: 'referencia',
    ag_x00ea_ncia: 'operacao',
    'agência': 'operacao',
    opera_x00e7__x00e3_o: 'operacao',
    'operação': 'operacao',
    consultornome: 'consultor',
    probabilidadedeconcretiza_x00e7_: 'probabilidade',
    probabilidadedeconcretiza_x00e7__x00e3_o: 'probabilidade',
    naturezadeneg_x00f3_cio: 'naturezaNegocio',
    natureza: 'naturezaNegocio',
    comiss_x00e3_ototalhabita: 'comissaoTotalHabita',
    dataPrevista_x002f_efetivaconclu: 'dataPrevistaFaturacao',
    dataprevista_x002f_efetivaconclu: 'dataPrevistaFaturacao',
    dataprevistaderecebimento: 'dataPrevistaRecebimento',
    refer_x00ea_ncia: 'referencia',
    ano: 'ano',
    m_x00ea_s: 'mes',
    mes: 'mes',
    n_x00ba_semana: 'numeroSemana',
    numerosemana: 'numeroSemana',
  };
  if (exact[lower]) return exact[lower];
  if (lower.includes('ag_x00ea') || lower.includes('agenc') || lower === 'operação') return 'operacao';
  if (lower.includes('consultor') && !lower.includes('email')) return 'consultor';
  if (lower.includes('probabil')) return 'probabilidade';
  if (lower.includes('natureza')) return 'naturezaNegocio';
  if (lower.includes('comiss')) return 'comissaoTotalHabita';
  if (lower.includes('refer')) return 'referencia';
  if (lower.includes('montante')) return 'montante';
  if (lower.includes('previst') && (lower.includes('fatur') || lower.includes('efetiv') || lower.includes('conclu'))) {
    return 'dataPrevistaFaturacao';
  }
  if (lower.includes('receb')) return 'dataPrevistaRecebimento';
  if (lower === 'estado') return 'estado';
  if (lower === 'data') return 'data';
  if (lower === 'ano' || (lower.includes('ano') && !lower.includes('semana'))) return 'ano';
  if (lower.includes('semana')) return 'numeroSemana';
  if (lower === 'mes' || lower.includes('m_x00ea_s')) return 'mes';
  return key;
}

function normalizeMediacaoNegociosColumns(defs) {
  return (defs || []).map((col, i) => {
    const spKey = col.key;
    const aliasKey = mapPipelineFieldToNegocioKey(spKey);
    return {
      ...col,
      // Keep SharePoint field key so values resolve from item.fields
      key: spKey,
      aliasKey: aliasKey && aliasKey !== spKey ? aliasKey : null,
      label: col.label || humanizeSpFieldKey(spKey || ''),
      order: col.order ?? i,
    };
  }).filter(c => c.key);
}

function RawInspectPanel({ item, onClose }) {
  const id = item?.id || item?.Id;
  const embeddedRaw = item?.rawFields || item?.RawFields || null;
  const [raw, setRaw] = useState(embeddedRaw);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      if (!id) {
        setLoading(false);
        return;
      }
      try {
        setLoading(true);
        // Same Graph FieldValueSet used by column-mapper "Raw (API)" — LookupIds, not resolved names.
        const sample = await apiFetch(`/api/mediacao/debug/pipeline-fields/${encodeURIComponent(id)}`);
        if (cancelled) return;
        setRaw(sample.fields || sample.Fields || embeddedRaw);
        setError(null);
      } catch (e) {
        if (cancelled) return;
        if (!embeddedRaw) setError(e.message || 'Erro ao carregar raw');
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, [id]);

  const payload = raw && typeof raw === 'object'
    ? { id, ...raw }
    : { id, note: 'Sem campos raw', dto: item };
  let text = '';
  try {
    text = JSON.stringify(payload, null, 2);
  } catch {
    text = String(payload);
  }

  const dismiss = useOverlayDismiss(onClose);

  return ReactDOM.createPortal(
    <div className="negocio-modal-overlay" role="presentation" {...dismiss}>
      <div
        className="negocio-modal raw-inspect-modal"
        role="dialog"
        aria-modal="true"
        aria-label="Dados raw SharePoint"
        onClick={e => e.stopPropagation()}
      >
        <div className="negocio-modal-panel">
          <div className="negocio-modal-header">
            <div>
              <h3>Raw SharePoint (Graph)</h3>
              <p className="negocio-modal-subtitle">
                ID {id || '—'} · FieldValueSet original (LookupId, etc.) · Admin
              </p>
            </div>
            <button type="button" className="negocio-modal-close" onClick={onClose} aria-label="Fechar">
              <Icon name="close" size={16} />
            </button>
          </div>
          <div className="negocio-modal-body">
            {loading && <LoadingSpinner message="A carregar raw…" />}
            {error && <p className="form-hint" style={{ color: '#b91c1c' }}>{error}</p>}
            {!loading && <pre className="raw-inspect-pre">{text}</pre>}
          </div>
          <div className="negocio-modal-footer">
            <button
              type="button"
              className="btn btn-secondary"
              disabled={loading}
              onClick={() => {
                try { navigator.clipboard?.writeText(text); } catch { /* ignore */ }
              }}
            >
              Copiar JSON
            </button>
            <button type="button" className="btn btn-primary" onClick={onClose}>Fechar</button>
          </div>
        </div>
      </div>
    </div>,
    document.body
  );
}

const MEDIACAO_NEGOCIOS_FILTERS_KEY = 'myhabita.mediacao.negocios.filters.v2';
const MEDIACAO_NEGOCIOS_GRID_KEY = 'pipeline-negocios';
const MEDIACAO_NEGOCIOS_URL_KEYS = Object.freeze([
  'q', 'ano', 'mes', 'estado', 'operacao', 'natureza', 'probabilidade',
]);

function defaultMediacaoNegociosFilters(currentYear = String(new Date().getFullYear())) {
  return {
    operacao: '',
    estado: '',
    naturezaNegocio: '',
    probabilidade: '',
    consultorSearch: '',
    ano: String(currentYear),
    mes: '',
  };
}

function mediacaoNegociosFiltersFromUrl(urlFilters) {
  if (!urlFilters || typeof urlFilters !== 'object') return {};
  const out = {};
  if (Object.prototype.hasOwnProperty.call(urlFilters, 'operacao')) out.operacao = String(urlFilters.operacao || '');
  if (Object.prototype.hasOwnProperty.call(urlFilters, 'estado')) out.estado = String(urlFilters.estado || '');
  if (Object.prototype.hasOwnProperty.call(urlFilters, 'natureza')) out.naturezaNegocio = String(urlFilters.natureza || '');
  if (Object.prototype.hasOwnProperty.call(urlFilters, 'probabilidade')) out.probabilidade = String(urlFilters.probabilidade || '');
  if (Object.prototype.hasOwnProperty.call(urlFilters, 'q')) out.consultorSearch = String(urlFilters.q || '');
  if (Object.prototype.hasOwnProperty.call(urlFilters, 'ano')) out.ano = String(urlFilters.ano || '');
  if (Object.prototype.hasOwnProperty.call(urlFilters, 'mes')) out.mes = String(urlFilters.mes || '');
  return out;
}

function mediacaoNegociosFiltersToUrl(filters) {
  return {
    q: filters?.consultorSearch || '',
    ano: filters?.ano || '',
    mes: filters?.mes || '',
    estado: filters?.estado || '',
    operacao: filters?.operacao || '',
    natureza: filters?.naturezaNegocio || '',
    probabilidade: filters?.probabilidade || '',
  };
}

function loadMediacaoNegociosFilters(currentYear = String(new Date().getFullYear())) {
  const defaults = defaultMediacaoNegociosFilters(currentYear);
  try {
    const raw = window.localStorage.getItem(MEDIACAO_NEGOCIOS_FILTERS_KEY);
    if (!raw) return defaults;
    const parsed = JSON.parse(raw);
    if (!parsed || typeof parsed !== 'object') return defaults;
    const merged = { ...defaults, ...parsed };
    // Blank year means "all years"; keep it if the user chose it. Missing/null → current year.
    if (merged.ano == null || merged.ano === undefined) merged.ano = String(currentYear);
    else merged.ano = String(merged.ano);
    return merged;
  } catch {
    return defaults;
  }
}

function initialMediacaoNegociosFilters(currentYear = String(new Date().getFullYear())) {
  const stored = loadMediacaoNegociosFilters(currentYear);
  const fromUrl = mediacaoNegociosFiltersFromUrl(parsePageFiltersFromSearch());
  if (Object.keys(fromUrl).length === 0) return stored;
  // URL wins for keys present in the query string.
  return { ...stored, ...fromUrl };
}

function saveMediacaoNegociosFilters(filters) {
  try {
    window.localStorage.setItem(MEDIACAO_NEGOCIOS_FILTERS_KEY, JSON.stringify(filters || {}));
  } catch { /* ignore */ }
}

function MediacaoPage({ user, activeTab, onTabChange, onConfigureListColumns }) {
  const mediacaoTab = normalizeRouteTab('mediacao', activeTab) || 'negocios';
  const viewportMode = useViewportMode();
  const isPhone = viewportMode === 'phone';
  const [phoneListView, setPhoneListView] = usePhoneListView();
  const useCards = isPhone && phoneListView === 'cards';
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);
  const [refreshing, setRefreshing] = useState(false);
  const [error, setError] = useState(null);
  const [selected, setSelected] = useState(null);
  const [activeRow, setActiveRow] = useState(null);
  const [creating, setCreating] = useState(() => new URLSearchParams(window.location.search).get('novo') === '1');
  const [deleteTarget, setDeleteTarget] = useState(null);
  const [deleting, setDeleting] = useState(false);
  const [saving, setSaving] = useState(false);
  const [inspectRaw, setInspectRaw] = useState(null);
  const [pageToken, setPageToken] = useState(null);
  const [currentToken, setCurrentToken] = useState(null);
  const [prevTokens, setPrevTokens] = useState([]);
  const [totalCount, setTotalCount] = useState(null);
  const currentYear = String(new Date().getFullYear());
  const [filters, setFilters] = useState(() => initialMediacaoNegociosFilters(currentYear));
  const [appliedFilters, setAppliedFilters] = useState(() => initialMediacaoNegociosFilters(currentYear));
  const [gridSort, setGridSort] = useState(() => loadGridViewState(MEDIACAO_NEGOCIOS_GRID_KEY)?.sort || { key: null, dir: 'asc' });
  const [gridSearch, setGridSearch] = useState(() => loadGridViewState(MEDIACAO_NEGOCIOS_GRID_KEY)?.globalQuery || '');
  const [gridColumnFilters, setGridColumnFilters] = useState(() => loadGridViewState(MEDIACAO_NEGOCIOS_GRID_KEY)?.filters || {});
  const [gridViewNonce, setGridViewNonce] = useState(0);
  const [pageIndex, setPageIndex] = useState(0);
  const [gridColumns, setGridColumns] = useState(MEDIACAO_NEGOCIOS_FALLBACK_COLUMNS);
  const [cardLayout, setCardLayout] = useState(() => emptyPhoneCardLayout());
  const hasLoadedRef = useRef(false);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      const { defs: fetched, cardLayout: fetchedLayout } = await fetchListGridColumns('pipeline', MEDIACAO_NEGOCIOS_FALLBACK_COLUMNS);
      const defs = normalizeMediacaoNegociosColumns(fetched);
      let withChoices = defs;
      try {
        const spCols = await apiFetch('/api/settings/lists/by-key/pipeline/columns');
        withChoices = attachMediacaoChoiceOptions(defs, Array.isArray(spCols) ? spCols : []);
      } catch { /* keep fallback choices */ }
      if (!cancelled) {
        setGridColumns(withChoices);
        setCardLayout(fetchedLayout || emptyPhoneCardLayout());
      }
    })();
    return () => { cancelled = true; };
  }, []);

  const handleGridColumnFilters = useCallback((nextFilters) => {
    setGridColumnFilters(nextFilters || {});
    setPrevTokens([]);
    setPageIndex(0);
    setFilters(prev => {
      const next = {
        ...prev,
        estado: pickGridEqualsFilterFor(nextFilters, gridColumns, 'estado', 'Estado'),
        naturezaNegocio: pickGridEqualsFilterFor(nextFilters, gridColumns, 'naturezaNegocio', 'Naturezadeneg_x00f3_cio'),
        probabilidade: pickGridEqualsFilterFor(nextFilters, gridColumns, 'probabilidade', 'ProbabilidadedeConcretiza_x00e7_'),
        operacao: pickGridEqualsFilterFor(nextFilters, gridColumns, 'operacao', 'Ag_x00ea_ncia'),
      };
      if (
        next.estado === prev.estado &&
        next.naturezaNegocio === prev.naturezaNegocio &&
        next.probabilidade === prev.probabilidade &&
        next.operacao === prev.operacao
      ) {
        return prev;
      }
      return next;
    });
  }, [gridColumns]);

  const handleGridSortChange = useCallback((nextSort) => {
    setGridSort(nextSort?.key ? { key: nextSort.key, dir: nextSort.dir || 'asc' } : { key: null, dir: 'asc' });
    setPrevTokens([]);
    setPageIndex(0);
  }, []);

  const handleGridSearchChange = useCallback((query) => {
    setGridSearch(String(query || ''));
    setPrevTokens([]);
    setPageIndex(0);
  }, []);

  // Auto-apply filters after typing/selecting (debounce text)
  useEffect(() => {
    const timer = setTimeout(() => {
      setAppliedFilters(prev => {
        const same = JSON.stringify(prev) === JSON.stringify(filters);
        return same ? prev : filters;
      });
      saveMediacaoNegociosFilters(filters);
      if (mediacaoTab === 'negocios') {
        replaceManagedPageFiltersInUrl(mediacaoNegociosFiltersToUrl(filters), MEDIACAO_NEGOCIOS_URL_KEYS);
      }
      setPrevTokens([]);
      setPageIndex(0);
    }, 400);
    return () => clearTimeout(timer);
  }, [filters, mediacaoTab]);

  const resetNegociosView = useCallback(() => {
    const defaults = defaultMediacaoNegociosFilters(currentYear);
    clearGridViewState(MEDIACAO_NEGOCIOS_GRID_KEY);
    saveMediacaoNegociosFilters(defaults);
    setFilters(defaults);
    setAppliedFilters(defaults);
    replaceManagedPageFiltersInUrl(mediacaoNegociosFiltersToUrl(defaults), MEDIACAO_NEGOCIOS_URL_KEYS);
    setGridSort({ key: null, dir: 'asc' });
    setGridSearch('');
    setGridColumnFilters({});
    setGridViewNonce(n => n + 1);
    setPrevTokens([]);
    setPageIndex(0);
    setActiveRow(null);
  }, [currentYear]);

  const loadData = useCallback(async (token = null, pageIdx = 0) => {
    const soft = hasLoadedRef.current;
    if (soft) setRefreshing(true);
    else setLoading(true);
    setError(null);
    try {
      const params = new URLSearchParams({ pageSize: '24' });
      if (token) params.set('pageToken', token);
      if (appliedFilters.operacao) params.set('operacao', appliedFilters.operacao);
      if (appliedFilters.estado) params.set('estado', appliedFilters.estado);
      if (appliedFilters.naturezaNegocio) params.set('naturezaNegocio', appliedFilters.naturezaNegocio);
      if (appliedFilters.probabilidade) params.set('probabilidade', appliedFilters.probabilidade);
      if (appliedFilters.consultorSearch) params.set('consultorSearch', appliedFilters.consultorSearch);
      if (appliedFilters.ano) params.set('ano', appliedFilters.ano);
      if (appliedFilters.mes) params.set('mes', appliedFilters.mes);
      if (gridSort.key) {
        params.set('orderBy', gridSort.key);
        params.set('orderDir', gridSort.dir || 'asc');
      }
      if (gridSearch.trim()) params.set('search', gridSearch.trim());
      const activeColumnFilters = Object.fromEntries(
        Object.entries(gridColumnFilters || {}).filter(([, f]) => columnFilterIsActive(f))
      );
      if (Object.keys(activeColumnFilters).length > 0) {
        params.set('columnFilters', JSON.stringify(activeColumnFilters));
      }
      if (soft && !token) params.set('refresh', 'true');

      const result = await apiFetch(`/api/mediacao/negocios?${params}`);
      setData(result.items || []);
      setPageToken(result.nextPageToken ?? null);
      setTotalCount(result.totalCount ?? null);
      setPageIndex(pageIdx);
      setCurrentToken(token);
      hasLoadedRef.current = true;
    } catch (err) {
      setError({ message: err.message, code: err.code, reconsentUrl: err.reconsentUrl });
    } finally {
      setLoading(false);
      setRefreshing(false);
    }
  }, [appliedFilters, gridSort, gridSearch, gridColumnFilters]);

  useEffect(() => {
    loadData(null, 0);
    setPrevTokens([]);
  }, [loadData]);

  const handleNextPage = () => {
    if (!pageToken || loading) return;
    setPrevTokens(prev => [...prev, currentToken]);
    loadData(pageToken, pageIndex + 1);
  };

  const handlePrevPage = () => {
    if (prevTokens.length === 0 || loading) return;
    const tokens = [...prevTokens];
    const prevToken = tokens.pop();
    setPrevTokens(tokens);
    loadData(prevToken ?? null, Math.max(0, pageIndex - 1));
  };

  const negociosPageSize = 24;
  const negociosPageCount = totalCount != null
    ? Math.max(1, Math.ceil(Number(totalCount) / negociosPageSize))
    : null;

  const handleSave = async (update) => {
    if (!selected) return;
    setSaving(true);
    try {
      const updated = await apiFetch(`/api/mediacao/negocios/${selected.id}`, {
        method: 'PATCH',
        body: JSON.stringify(update),
      });
      setData(prev => prev.map(n => n.id === updated.id ? updated : n));
      setSelected(updated);
      toast.success('Negócio atualizado.');
    } catch (err) {
      toast.error('Erro ao guardar: ' + err.message);
    } finally {
      setSaving(false);
    }
  };

  const handleCreate = async (payload) => {
    setSaving(true);
    try {
      await apiFetch('/api/mediacao/negocios', {
        method: 'POST',
        body: JSON.stringify(payload),
      });
      setCreating(false);
      loadData(null, 0);
      setPrevTokens([]);
      toast.success('Negócio criado.');
    } catch (err) {
      toast.error('Erro ao criar: ' + err.message);
    } finally {
      setSaving(false);
    }
  };

  const requestDelete = (row) => {
    if (!row) return;
    setDeleteTarget(row);
  };

  const confirmDelete = async () => {
    const row = deleteTarget;
    const id = row?.id ?? row?.Id;
    if (!id) {
      setDeleteTarget(null);
      return;
    }
    setDeleting(true);
    try {
      await apiFetch(`/api/mediacao/negocios/${id}`, { method: 'DELETE' });
      if ((selected?.id ?? selected?.Id) === id) setSelected(null);
      if ((activeRow?.id ?? activeRow?.Id) === id) setActiveRow(null);
      setDeleteTarget(null);
      loadData(currentToken, pageIndex);
      toast.success('Negócio eliminado.');
    } catch (err) {
      toast.error('Erro ao eliminar: ' + err.message);
    } finally {
      setDeleting(false);
    }
  };

  const canDeleteNegocio = !!(user?.canDelete || user?.CanDelete || userIsAdmin(user));
  const canUpdateNegocio = !!(user?.canUpdate || user?.CanUpdate || userIsAdmin(user));
  const hasRowSelection = !!activeRow;
  const displayGridColumns = useMemo(
    () => (isPhone && phoneListView === 'table' ? slimColumnsForPhone(gridColumns, 5) : gridColumns),
    [isPhone, phoneListView, gridColumns]
  );

  const openSelectedEditor = () => {
    if (!activeRow) return;
    setCreating(false);
    setSelected(activeRow);
  };

  const toggleRowSelection = (row) => {
    const id = row?.id ?? row?.Id;
    const currentId = activeRow?.id ?? activeRow?.Id;
    setActiveRow(currentId != null && String(currentId) === String(id) ? null : row);
  };

  const meses = ['Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'];
  const estadoOptions = (() => {
    const fromCol = gridColumns.find(c => {
      const k = String(c.key || '').toLowerCase();
      const alias = String(c.aliasKey || '').toLowerCase();
      return k === 'estado' || alias === 'estado';
    });
    const choices = fromCol?.choices || fromCol?.Choices;
    if (Array.isArray(choices) && choices.length) return choices.map(String);
    return ['Em Curso', 'Concluído', 'Perdido'];
  })();

  const tabsSlot = <MediacaoTabsBar mediacaoTab={mediacaoTab} onTabChange={onTabChange} />;

  if (mediacaoTab === 'comissao') {
    return (
      <>
        <ModuleTopbar icon="mediacao" title="Mediação" user={user} />
        <GenericListPage
          key="comissao"
          user={user}
          listKey="comissoes"
          columns={COMISSOES_COLUMNS}
          hideHeader
          headerSlot={tabsSlot}
          onConfigureListColumns={onConfigureListColumns}
          enableEstadoFilter
          estadoFilterField="validacao"
          estadoFilterOptions={['Pendente', 'Aprovado', 'Reprovado']}
          enableLiquidadoFilter
          liquidadoFilterOptions={['Sim', 'Não']}
          enablePeriodFilter
          periodDateField="Data"
          enableOperacaoFilter={userIsAdmin(user) || userIsDiretorGeral(user)}
          walletMode
        />
      </>
    );
  }

  if (mediacaoTab === 'faturacao') {
    return (
      <>
        <ModuleTopbar icon="mediacao" title="Mediação" user={user} />
        <FaturacaoPage user={user} headerSlot={tabsSlot} onConfigureListColumns={onConfigureListColumns} />
      </>
    );
  }

  return (
    <>
      <ModuleTopbar
        icon="mediacao"
        title="Mediação"
        user={user}
      />
      <div className="page-body">
        <div className="page-content mediacao-layout">
          <div className="content-main">
            <ModulePageShell
              tabs={tabsSlot}
              toolbar={(
                <div className="toolbar mediacao-toolbar">
                  <div className="toolbar-filters">
                    {!userIsConsultor(user) && (
                      <input
                        placeholder="Pesquisar Consultor"
                        aria-label="Pesquisar Consultor"
                        value={filters.consultorSearch}
                        onChange={e => setFilters({ ...filters, consultorSearch: e.target.value })}
                      />
                    )}
                    <AppSelect
                      variant="toolbar"
                      value={filters.mes}
                      emptyOption="Mês (todos)"
                      options={meses.map((m, i) => ({ value: i + 1, label: m }))}
                      onChange={v => setFilters({ ...filters, mes: v })}
                      ariaLabel="Mês"
                    />
                    <AppSelect
                      variant="toolbar"
                      value={filters.ano}
                      emptyOption="Ano (todos)"
                      options={[...new Set([2024, 2025, 2026, 2027, Number(currentYear)])].sort((a, b) => a - b).map(y => ({ value: y, label: String(y) }))}
                      onChange={v => setFilters({ ...filters, ano: v })}
                      ariaLabel="Ano"
                    />
                    <AppSelect
                      variant="toolbar"
                      value={filters.estado}
                      emptyOption="Estado (todos)"
                      options={estadoOptions}
                      onChange={v => setFilters({ ...filters, estado: v })}
                      ariaLabel="Estado"
                    />
                    <button type="button" className="btn btn-secondary toolbar-icon-btn" onClick={() => loadData(currentToken, pageIndex)} title="Atualizar">
                      <Icon name="refresh" size={16} />
                    </button>
                    <button
                      type="button"
                      className="btn btn-secondary toolbar-icon-btn"
                      onClick={resetNegociosView}
                      title="Repor filtros e ordenação"
                    >
                      <Icon name="filterClear" size={16} />
                    </button>
                    {isPhone && (
                      <PhoneListViewToggle view={phoneListView} onChange={setPhoneListView} />
                    )}
                  </div>
                  <div className="toolbar-actions">
                    <MapearColunasToolbarButton
                      user={user}
                      listKey="pipeline"
                      onConfigureListColumns={onConfigureListColumns}
                    />
                    {userIsAdmin(user) && (
                      <button
                        type="button"
                        className="btn btn-secondary"
                        disabled={!hasRowSelection}
                        title={hasRowSelection ? 'Ver dados raw (Admin)' : 'Selecione um negócio'}
                        onClick={() => activeRow && setInspectRaw(activeRow)}
                      >
                        <Icon name="eye" size={15} />
                        <span className="btn-label-text">Ver</span>
                      </button>
                    )}
                    <button
                      type="button"
                      className="btn btn-secondary"
                      disabled={!hasRowSelection}
                      title={hasRowSelection
                        ? (canUpdateNegocio ? 'Editar negócio selecionado' : 'Ver negócio selecionado')
                        : 'Selecione um negócio'}
                      onClick={openSelectedEditor}
                    >
                      <Icon name={canUpdateNegocio ? 'edit' : 'eye'} size={15} />
                      <span className="btn-label-text">{canUpdateNegocio ? 'Editar' : 'Ver'}</span>
                    </button>
                    {canDeleteNegocio && (
                      <button
                        type="button"
                        className="btn btn-secondary"
                        disabled={!hasRowSelection}
                        title={hasRowSelection ? 'Eliminar negócio selecionado' : 'Selecione um negócio'}
                        onClick={() => requestDelete(activeRow)}
                      >
                        <Icon name="trash" size={15} />
                        <span className="btn-label-text">Eliminar</span>
                      </button>
                    )}
                    <button
                      type="button"
                      className="btn btn-primary"
                      onClick={() => { setSelected(null); setCreating(true); }}
                    >
                      <Icon name="plus" size={16} color="#FFFFFF" />
                      <span className="btn-label-text">Novo</span>
                    </button>
                  </div>
                </div>
              )}
            >
              {loading && <LoadingSpinner message="A carregar..." />}
              {error && <ErrorBanner error={error} onDismiss={() => setError(null)} />}
              {!loading && !error && (
                <>
                  <ListTotalsCards rows={data} columnDefs={gridColumns} />
                  <div className={`module-page-table${refreshing ? ' is-refreshing' : ''}${useCards ? ' module-page-table-cards' : ''}`}>
                  {refreshing && (
                    <div className="data-grid-refresh-hint" aria-live="polite">A atualizar dados…</div>
                  )}
                  {useCards ? (
                    <DataListCards
                      columns={displayGridColumns}
                      rows={data}
                      cardLayout={cardLayout}
                      selectedId={activeRow?.id ?? activeRow?.Id}
                      onRowClick={toggleRowSelection}
                      onRowDoubleClick={(row) => {
                        setActiveRow(row);
                        setCreating(false);
                        setSelected(row);
                      }}
                      emptyMessage="Sem negócios."
                      renderCardActions={(row) => (
                        <>
                          {userIsAdmin(user) && (
                            <button
                              type="button"
                              className="btn btn-secondary btn-sm"
                              onClick={() => setInspectRaw(row)}
                              title="Ver raw"
                            >
                              <Icon name="eye" size={14} />
                            </button>
                          )}
                          <button
                            type="button"
                            className="btn btn-secondary btn-sm"
                            onClick={() => {
                              setActiveRow(row);
                              setCreating(false);
                              setSelected(row);
                            }}
                            title={canUpdateNegocio ? 'Editar' : 'Ver'}
                          >
                            <Icon name={canUpdateNegocio ? 'edit' : 'eye'} size={14} />
                          </button>
                          {canDeleteNegocio && (
                            <button
                              type="button"
                              className="btn btn-secondary btn-sm"
                              onClick={() => requestDelete(row)}
                              title="Eliminar"
                            >
                              <Icon name="trash" size={14} />
                            </button>
                          )}
                        </>
                      )}
                    />
                  ) : (
                    <DataGrid
                      key={`pipeline-negocios-${gridViewNonce}`}
                      storageKey={MEDIACAO_NEGOCIOS_GRID_KEY}
                      columns={displayGridColumns}
                      rows={data}
                      selectedId={activeRow?.id ?? activeRow?.Id}
                      onRowClick={toggleRowSelection}
                      onRowDoubleClick={(row) => {
                        setActiveRow(row);
                        setCreating(false);
                        setSelected(row);
                      }}
                      emptyMessage="Sem negócios."
                      filterMode="server"
                      preferSourceOrder
                      showColumnChooser={!isPhone}
                      onColumnFiltersChange={handleGridColumnFilters}
                      onSortChange={handleGridSortChange}
                      onGlobalQueryChange={handleGridSearchChange}
                    />
                  )}
                  <div className="pagination">
                    <span>
                      {totalCount != null ? `Linhas: ${data.length} / ${totalCount} · ` : `Linhas: ${data.length} · `}
                      Página {pageIndex + 1}{negociosPageCount != null ? ` / ${negociosPageCount}` : ''}
                    </span>
                    <span>
                      <AppTooltip label="Anterior">
                        <button type="button" className="btn btn-secondary btn-icon" onClick={handlePrevPage} disabled={prevTokens.length === 0 || loading} aria-label="Anterior">
                          <Icon name="arrowLeft" size={16} />
                        </button>
                      </AppTooltip>
                      {' '}
                      <AppTooltip label="Seguinte">
                        <button type="button" className="btn btn-secondary btn-icon" onClick={handleNextPage} disabled={!pageToken || loading} aria-label="Seguinte">
                          <Icon name="arrowRight" size={16} />
                        </button>
                      </AppTooltip>
                    </span>
                  </div>
                </div>
                </>
              )}
            </ModulePageShell>
          </div>
        </div>
        <NegocioModalOverlay
          open={creating}
          onClose={() => { if (!saving) setCreating(false); }}
          label="Novo negócio"
        >
          <CreateNegocioPanel
            user={user}
            onCreate={handleCreate}
            onClose={() => setCreating(false)}
            saving={saving}
          />
        </NegocioModalOverlay>
        <NegocioModalOverlay
          open={!!selected && !creating}
          onClose={() => { if (!saving) setSelected(null); }}
          label="Atualizar negócio"
        >
          <UpdatePanel
            item={selected}
            onSave={handleSave}
            onClose={() => setSelected(null)}
            saving={saving}
          />
        </NegocioModalOverlay>
        {inspectRaw && (
          <RawInspectPanel item={inspectRaw} onClose={() => setInspectRaw(null)} />
        )}
        <ConfirmDialog
          open={!!deleteTarget}
          title="Eliminar negócio"
          message={(
            <>
              Tem a certeza que pretende eliminar
              {' '}
              <strong>
                {deleteTarget?.referencia
                  || deleteTarget?.Referencia
                  || deleteTarget?.title
                  || deleteTarget?.Title
                  || 'este negócio'}
              </strong>
              ? Esta ação não pode ser anulada.
            </>
          )}
          confirmLabel="Eliminar"
          cancelLabel="Cancelar"
          danger
          busy={deleting}
          onCancel={() => { if (!deleting) setDeleteTarget(null); }}
          onConfirm={confirmDelete}
        />
      </div>
    </>
  );
}

const SITE_LIST_PICKER_COLUMNS = [
  {
    key: 'enabled',
    label: 'Active',
    visible: true,
    order: 0,
    width: 88,
    fixed: true,
    filterType: 'choice',
    choices: [
      { value: 'true', label: 'Ativa' },
      { value: 'false', label: 'Inativa' },
    ],
  },
  { key: 'displayName', label: 'Lista', visible: true, order: 1, fill: true },
  { key: 'grelha', label: 'Grelha', visible: true, order: 2, width: 120, filterable: false, sortable: false },
];

function foldListQuery(value) {
  return String(value || '')
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .toLowerCase()
    .trim();
}

function SiteListPicker({ site, lists, onChange, loading, error, onLoad, onOpenColumns }) {
  const [query, setQuery] = useState('');

  const toggleEnabled = (key) => {
    onChange(lists.map(l => l.key === key ? { ...l, enabled: !l.enabled } : l));
  };

  const rows = useMemo(
    () => [...lists]
      .sort((a, b) => (a.displayName || '').localeCompare(b.displayName || '', 'pt', { sensitivity: 'base' }))
      .map(list => {
        const configured = list.gridColumns || list.GridColumns || [];
        const visible = configured.filter(c => {
          const v = c.visible ?? c.Visible;
          return v !== false; // treat undefined as visible
        });
        const ordered = visible
          .slice()
          .sort((a, b) => (a.order ?? a.Order ?? 0) - (b.order ?? b.Order ?? 0));
        const labels = ordered
          .map(c => c.label || c.Label || humanizeSpFieldKey(c.key || c.Key || ''))
          .filter(Boolean)
          .slice(0, 3);
        const grelhaSummary = labels.length ? labels.join(', ') : 'Colunas';

        return {
          ...list,
          id: `${site.key}-${list.listId || list.key}`,
          enabled: !!list.enabled,
          displayName: list.displayName || list.key,
          grelha: list.enabled ? grelhaSummary : '',
        };
      }),
    [lists, site.key]
  );

  const filteredRows = useMemo(() => {
    const q = foldListQuery(query);
    if (!q) return rows;
    return rows.filter(row =>
      foldListQuery(row.displayName).includes(q) || foldListQuery(row.key).includes(q)
    );
  }, [rows, query]);

  return (
    <div className="site-list-picker">
      <div className="site-list-picker-header">
        <span className="form-hint">Marque as listas que a app pode usar neste site (CRUD). Configure as colunas da grelha por lista.</span>
        <div className="site-list-picker-actions">
          {lists.length > 0 ? (
            <input
              type="search"
              className="site-list-picker-search"
              placeholder="Pesquisar lista"
              aria-label={`Pesquisar listas — ${site.name}`}
              value={query}
              onChange={e => setQuery(e.target.value)}
            />
          ) : null}
          <button
            type="button"
            className="btn btn-secondary btn-sm"
            onClick={onLoad}
            disabled={loading}
          >
            {loading ? 'A carregar...' : `Carregar listas — ${site.name}`}
          </button>
        </div>
      </div>
      {error && <div className="test-result fail" style={{ marginTop: '0.5rem' }}>✗ {error}</div>}
      {rows.length === 0 ? (
        <p className="form-hint" style={{ marginTop: '0.75rem' }}>
          Nenhuma lista carregada para {site.name}. Indique o URL e clique em «Carregar listas».
        </p>
      ) : (
        <div className="list-picker-wrap list-picker-grid" style={{ marginTop: '0.75rem' }}>
          <DataGrid
            storageKey={`settings-lists-${site.key}`}
            columns={SITE_LIST_PICKER_COLUMNS}
            rows={filteredRows}
            emptyMessage={query.trim() ? 'Nenhuma lista corresponde à pesquisa.' : 'Sem listas.'}
            showColumnChooser={false}
            renderCell={(row, col) => {
              if (col.key === 'enabled') {
                return (
                  <input
                    type="checkbox"
                    checked={!!row.enabled}
                    onChange={() => toggleEnabled(row.key)}
                    aria-label={`Ativar ${row.displayName}`}
                  />
                );
              }
              if (col.key === 'displayName') {
                return (
                  <span className={row.enabled ? '' : 'list-picker-disabled-name'}>{row.displayName}</span>
                );
              }
              if (col.key === 'grelha') {
                if (!row.enabled) return null;
                return (
                  <button
                    type="button"
                    className="btn btn-secondary btn-sm"
                    onClick={() => onOpenColumns && onOpenColumns(row)}
                  >
                    {row.grelha || 'Colunas'}
                  </button>
                );
              }
              return undefined;
            }}
          />
        </div>
      )}
    </div>
  );
}

const GRID_COLUMN_EDITOR_COLUMNS = [
  { key: 'visivel', label: 'Visível', visible: true, order: 0, filterType: 'choice', choices: [{ value: 'true', label: 'Sim' }, { value: 'false', label: 'Não' }] },
  { key: 'campo', label: 'Campo', visible: true, order: 1 },
  { key: 'rotulo', label: 'Rótulo', visible: true, order: 2 },
  { key: 'displayPath', label: 'Mostrar', visible: true, order: 3 },
  { key: 'format', label: 'Formato', visible: true, order: 4, filterType: 'choice', choices: GRID_COLUMN_FORMAT_OPTIONS },
  { key: 'origem', label: 'Origem', visible: true, order: 5, filterType: 'choice', choices: ['Sistema', 'Utilizador'] },
  { key: 'tipo', label: 'Tipo SP', visible: true, order: 6, filterType: 'choice' },
  { key: 'raw', label: 'Raw (API)', visible: true, order: 7 },
  { key: 'preview', label: 'Preview', visible: true, order: 8 },
  { key: 'order', label: 'Ordem', visible: true, order: 9, filterable: false, sortable: false, width: 72 },
];

function formatSpColumnTipo(col) {
  const kind = (col.columnKind || col.ColumnKind || '').toLowerCase();
  if (kind === 'lookup') {
    const target = col.lookupListName || col.LookupListName || col.lookupListId || col.LookupListId || 'lista';
    const field = col.lookupColumnName || col.LookupColumnName;
    return field ? `Lookup → ${target} · ${field}` : `Lookup → ${target}`;
  }
  if (kind === 'calculated') {
    const formula = col.calculatedFormula || col.CalculatedFormula;
    if (formula) {
      const short = formula.length > 48 ? `${formula.slice(0, 48)}…` : formula;
      return `Calculada: ${short}`;
    }
    return 'Calculada';
  }
  if (kind === 'choice') return 'Choice';
  if (kind === 'person') return 'Person';
  if (kind === 'currency') return 'Currency (€)';
  if (kind === 'number') return 'Number';
  if (kind) return kind;
  return col.type || col.Type || '—';
}

function displayPathOptionLabel(path) {
  const p = String(path || '');
  if (!p) return '(valor completo)';
  switch (p.toLowerCase()) {
    case 'value':
    case 'lookupvalue': return 'Valor';
    case 'displayname': return 'Nome';
    case 'title': return 'Título';
    case 'email': return 'Email';
    case 'id': return 'Id';
    case 'address': return 'Morada';
    default: return humanizeSpFieldKey(p);
  }
}

function columnHasDisplayPathChoices(col) {
  const kind = String(col?.columnKind || col?.ColumnKind || '').toLowerCase();
  return kind === 'lookup' || kind === 'person' || kind === 'location' || kind === 'geolocation';
}

function defaultDisplayPathForColumn(col, fieldKey, samplePreview, sampleRaw) {
  const options = displayPathOptionsForColumn(col, fieldKey, samplePreview, sampleRaw).filter(Boolean);
  const kindDefault = defaultDisplayPathForSpKind(col);
  if (kindDefault && options.some(o => o.toLowerCase() === String(kindDefault).toLowerCase())) return kindDefault;
  if (options.includes('displayName')) return 'displayName';
  if (options.includes('lookupValue')) return 'lookupValue';
  if (options.includes('Title')) return 'Title';
  return '';
}

function displayPathOptionsForColumn(col) {
  const options = [''];
  const kind = String(col?.columnKind || col?.ColumnKind || '').toLowerCase();
  if (kind === 'lookup') {
    const lookupCol = col?.lookupColumnName || col?.LookupColumnName;
    options.push('lookupValue');
    if (lookupCol && !['lookupvalue', 'value'].includes(String(lookupCol).toLowerCase())) {
      options.push(lookupCol);
    }
  } else if (kind === 'person') {
    options.push('displayName', 'email');
  } else if (kind === 'location' || kind === 'geolocation') {
    options.push('DisplayName', 'Address');
  }
  const seen = new Set();
  return options.filter((p) => {
    const key = String(p || '').toLowerCase();
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

function getRawFieldSnapshot(rawFields, fieldKey) {
  if (!rawFields) return null;
  const exact = rawFields[fieldKey]
    ?? rawFields[Object.keys(rawFields).find(k => k.toLowerCase() === fieldKey.toLowerCase())];
  const lookupId = rawFields[`${fieldKey}LookupId`]
    ?? rawFields[Object.keys(rawFields).find(k => k.toLowerCase() === `${fieldKey}lookupid`)];
  const snapshot = {};
  if (exact != null && exact !== '') snapshot[fieldKey] = exact;
  if (lookupId != null && lookupId !== '') snapshot[`${fieldKey}LookupId`] = lookupId;
  return Object.keys(snapshot).length ? snapshot : null;
}

function formatRawFieldSnapshot(snapshot) {
  if (!snapshot) return '—';
  try {
    const keys = Object.keys(snapshot);
    if (keys.length === 1) {
      const v = snapshot[keys[0]];
      if (v != null && typeof v === 'object') return JSON.stringify(v).slice(0, 120);
      return String(v);
    }
    return JSON.stringify(snapshot).slice(0, 120);
  } catch {
    return '—';
  }
}

function PhoneCardLayoutEditor({
  columns,
  cardLayout,
  samplePreview,
  sampleRaw,
  onChange,
}) {
  const layout = normalizePhoneCardLayout(cardLayout);
  const columnOptions = useMemo(() => {
    const defs = toColumnDefs(columns);
    const selectedKeys = new Set(
      [layout.title, layout.slot1, layout.slot2, layout.slot3, layout.slot4]
        .map(k => String(k || '').toLowerCase())
        .filter(Boolean)
    );
    const seen = new Set();
    return defs
      .filter((c) => {
        const k = String(c.key);
        if (!k || seen.has(k.toLowerCase())) return false;
        seen.add(k.toLowerCase());
        if (c.system && c.visible === false && !selectedKeys.has(k.toLowerCase())) return false;
        return true;
      })
      .sort((a, b) => {
        const av = a.visible === false ? 1 : 0;
        const bv = b.visible === false ? 1 : 0;
        if (av !== bv) return av - bv;
        return String(a.label || a.key).localeCompare(String(b.label || b.key), 'pt');
      })
      .map(c => ({ key: c.key, label: c.label || c.key }));
  }, [columns, layout.title, layout.slot1, layout.slot2, layout.slot3, layout.slot4]);
  const resolved = resolvePhoneCardFields(columns, layout);
  const previewRow = useMemo(() => ({
    ...(samplePreview || {}),
    fields: { ...(sampleRaw || {}), ...(samplePreview || {}) },
  }), [samplePreview, sampleRaw]);

  const slotValue = (slotId) => {
    if (phoneCardLayoutIsEmpty(layout)) {
      if (slotId === 'title') return resolved.title?.key || '';
      const idx = Number(slotId.replace('slot', '')) - 1;
      return resolved.slots[idx]?.key || '';
    }
    return layout[slotId] || '';
  };

  const setSlot = (slotId, key) => {
    const next = phoneCardLayoutIsEmpty(layout)
      ? {
          title: resolved.title?.key || '',
          slot1: resolved.slots[0]?.key || '',
          slot2: resolved.slots[1]?.key || '',
          slot3: resolved.slots[2]?.key || '',
          slot4: resolved.slots[3]?.key || '',
        }
      : { ...layout };
    next[slotId] = key;
    onChange(next);
  };

  const renderSlotPreview = (colKey) => {
    const col = findColumnDefByKey(columns, colKey);
    if (!col) return <span className="phone-card-mapper-placeholder">—</span>;
    const text = formatDataListCardCell(previewRow, col) || '—';
    if (isDataListCardStatusColumn(col) && text && text !== '—') {
      return <span className={`status-badge ${statusClass(text)}`}>{text}</span>;
    }
    return <span>{text}</span>;
  };

  const slotSelect = (slotId, ariaLabel) => (
    <AppSelect
      layout="combo"
      className="phone-card-mapper-select"
      ariaLabel={ariaLabel}
      value={slotValue(slotId)}
      emptyOption={phoneCardLayoutIsEmpty(layout) ? 'Automático' : 'Não mostrar'}
      options={columnOptions.map(opt => ({ value: opt.key, label: opt.label }))}
      onChange={v => setSlot(slotId, v)}
    />
  );

  return (
    <section className="phone-card-mapper" aria-label="Cartão telemóvel">
      <div className="phone-card-mapper-copy">
        <h3>Cartão (telemóvel)</h3>
        <p>
          Escolha a coluna de cada zona do cartão. O exemplo usa o primeiro registo da lista.
          {phoneCardLayoutIsEmpty(layout) ? ' A predefinição automática está ativa até alterar um campo.' : ''}
        </p>
        {!phoneCardLayoutIsEmpty(layout) && (
          <button
            type="button"
            className="btn btn-secondary btn-sm"
            onClick={() => onChange(emptyPhoneCardLayout())}
          >
            Repor automático
          </button>
        )}
      </div>
      <article className="data-list-card phone-card-mapper-card" aria-hidden="false">
        <div className="data-list-card-body">
          <div className="phone-card-mapper-slot is-title">
            {slotSelect('title', 'Coluna do título')}
            <h4 className="data-list-card-title">{renderSlotPreview(slotValue('title'))}</h4>
          </div>
          <dl className="data-list-card-meta">
            {['slot1', 'slot2', 'slot3', 'slot4'].map((slotId, i) => (
              <div key={slotId} className="data-list-card-meta-row phone-card-mapper-slot">
                {slotSelect(slotId, `Coluna do campo ${i + 1}`)}
                <dd>{renderSlotPreview(slotValue(slotId))}</dd>
              </div>
            ))}
          </dl>
        </div>
        <div className="data-list-card-actions" aria-hidden="true">
          <span className="btn btn-secondary btn-sm" title="Editar"><Icon name="edit" size={14} /></span>
          <span className="btn btn-secondary btn-sm" title="Eliminar"><Icon name="trash" size={14} /></span>
        </div>
      </article>
    </section>
  );
}

function GridColumnsEditorPage({ list, spColumns, samplePreview, sampleRaw, loading, error, onBack, onChangeGridColumns, onChangeCardLayout = () => {}, onSave, saving, saveMessage }) {
  const configured = list.gridColumns || list.GridColumns || [];

  const previewValue = (fieldKey, displayPath, format, spCol) => {
    if (!samplePreview && !sampleRaw) return null;
    // Preview (enriched) overrides Raw, but raw sibling keys (e.g. LookupId) remain available for Mostrar.
    const item = { fields: { ...(sampleRaw || {}), ...(samplePreview || {}) } };
    const kind = (spCol?.columnKind || spCol?.ColumnKind || '').toLowerCase();
    if (kind === 'calculated') {
      const raw = getFieldValue(item, fieldKey);
      if (raw == null || raw === '') return null;
      return String(formatCellValue(raw, fieldKey, format)).slice(0, 80);
    }
    const resolved = getLookupDisplayValue(item, fieldKey, displayPath || defaultDisplayPathForColumn(spCol || {}, fieldKey, samplePreview, sampleRaw));
    if (resolved == null || resolved === '') return null;
    return String(formatCellValue(resolved, fieldKey, format)).slice(0, 80);
  };

  const setColProp = (fieldKey, patch) => {
    const current = [...configured];
    const idx = current.findIndex(c => (c.key || c.Key) === fieldKey);
    if (idx >= 0) {
      current[idx] = { ...current[idx], ...patch };
    } else {
      current.push({
        key: fieldKey,
        label: humanizeSpFieldKey(fieldKey),
        visible: false,
        order: current.length,
        displayPath: null,
        format: null,
        ...patch,
      });
    }
    current.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)).forEach((c, i) => { c.order = i; });
    onChangeGridColumns(current);
  };

  const colPatch = (row, extra = {}) => ({
    key: row.campo,
    label: row.rotulo,
    visible: !!row.visivel,
    displayPath: row.displayPath || null,
    format: row.format || null,
    order: row.order,
    ...extra,
  });

  const rows = (spColumns || [])
    .filter(c => {
      const name = c.name || c.Name || '';
      return name && !name.startsWith('_');
    })
    .map((col, i) => {
      const name = col.name || col.Name;
      const cfg = configured.find(c => (c.key || c.Key) === name) || {};
      const isSystem = !!(col.isSystem ?? col.IsSystem ?? col.readOnly ?? col.ReadOnly);
      const kind = String(col.columnKind || col.ColumnKind || '').toLowerCase();
      let displayPath = cfg.displayPath ?? cfg.DisplayPath ?? defaultDisplayPathForColumn(col, name, samplePreview, sampleRaw);
      if (kind === 'person' && (displayPath === 'value' || displayPath === 'lookupValue')) {
        displayPath = 'displayName';
      }
      if (kind === 'lookup' && (displayPath === 'displayName' || displayPath === 'value')) {
        displayPath = defaultDisplayPathForSpKind(col) || 'lookupValue';
      }
      if (!columnHasDisplayPathChoices(col) && (displayPath === 'value' || displayPath === name)) {
        displayPath = '';
      }
      const format = cfg.format ?? cfg.Format ?? defaultFormatForSpColumn(col, name) ?? '';
      const rawSnapshot = getRawFieldSnapshot(sampleRaw, name);
      const hasCfg = Object.keys(cfg).length > 0;
      return {
        id: name,
        campo: name,
        rotulo: cfg.label || cfg.Label || col.displayName || col.DisplayName || humanizeSpFieldKey(name),
        visivel: hasCfg ? !!(cfg.visible ?? cfg.Visible) : (!isSystem || name.toLowerCase() === 'title'),
        displayPath: displayPath || '',
        format,
        order: cfg.order ?? cfg.Order ?? i,
        origem: isSystem ? 'Sistema' : 'Utilizador',
        tipo: formatSpColumnTipo(col),
        raw: formatRawFieldSnapshot(rawSnapshot),
        preview: previewValue(name, displayPath, format, col) || '—',
        _isSystem: isSystem,
        _spCol: col,
      };
    })
    .sort((a, b) => (a.order - b.order) || String(a.campo).localeCompare(String(b.campo), 'pt'));

  const handleRowReorder = (fromId, toId) => {
    const list = rows.slice();
    const from = list.findIndex(r => String(r.campo) === String(fromId));
    const to = list.findIndex(r => String(r.campo) === String(toId));
    if (from < 0 || to < 0 || from === to) return;
    const [moved] = list.splice(from, 1);
    list.splice(to, 0, moved);

    const byKey = Object.fromEntries(
      (configured || []).map(c => [c.key || c.Key, c])
    );

    const next = list.map((row, i) => {
      const existing = byKey[row.campo] || {};
      return {
        key: row.campo,
        label: existing.label || existing.Label || row.rotulo,
        visible: existing.visible ?? existing.Visible ?? !!row.visivel,
        displayPath: (existing.displayPath ?? existing.DisplayPath ?? row.displayPath) || null,
        format: (existing.format ?? existing.Format ?? row.format) || null,
        order: i,
      };
    });
    onChangeGridColumns(next);
  };

  const handleMarkUtilizador = () => {
    onChangeGridColumns(applyUtilizadorVisibilityToGridColumns(configured, spColumns, true));
  };

  const handleResetDefaults = () => {
    if (!confirm('Repor mapeamento: marcar todas as colunas Utilizador e formatos por defeito?')) return;
    onChangeGridColumns(buildDefaultGridColumnsFromSp(spColumns));
  };

  return (
    <>
      <ModuleTopbar
        icon="settings"
        title={`Mapeamento de colunas — ${list.displayName}`}
        actions={(
          <>
            {saveMessage && (
              <span className="form-hint" style={{ marginRight: 8 }}>{saveMessage}</span>
            )}
            <button type="button" className="btn btn-secondary" onClick={onBack} disabled={saving}>
              Voltar às listas
            </button>
            <button type="button" className="btn btn-secondary" onClick={handleMarkUtilizador} disabled={saving || loading}>
              Marcar Utilizador
            </button>
            <button type="button" className="btn btn-secondary" onClick={handleResetDefaults} disabled={saving || loading}>
              Repor defeito
            </button>
            <button type="button" className="btn btn-primary" onClick={onSave} disabled={saving}>
              {saving ? 'A guardar…' : 'Guardar'}
            </button>
          </>
        )}
      />
      <div className="page-body">
        <div className="page-content module-layout settings-page settings-columns-page">
          <ModulePageShell>
          <p className="form-hint settings-columns-hint">
            Escolha as colunas visíveis, o rótulo, a propriedade a <strong>Mostrar</strong>
            (nome, título ou valor do objeto — não chaves internas como <code>field_10</code>)
            e o <strong>Formato</strong> de apresentação (ex.: Moeda → <code>2 500,00 €</code>).
            Arraste as linhas (ícone ☰) para definir a <strong>ordem</strong> das colunas na grelha.
            <strong> Marcar Utilizador</strong> ativa Visível em todas as colunas de origem Utilizador (como no Pipeline).
            Alterações são guardadas automaticamente. <strong>Raw (API)</strong> é o valor SharePoint antes da resolução;
            <strong> Preview</strong> aplica Mostrar + Formato.
          </p>
          {!loading && !error && (
            <PhoneCardLayoutEditor
              columns={rows.map(r => ({
                key: r.campo,
                label: r.rotulo,
                format: r.format,
                system: r._isSystem,
                visible: r.visivel,
              }))}
              cardLayout={list.cardLayout || list.CardLayout}
              samplePreview={samplePreview}
              sampleRaw={sampleRaw}
              onChange={onChangeCardLayout}
            />
          )}
          {loading && <LoadingSpinner message="A carregar colunas SharePoint…" />}
          {error && <ErrorBanner error={{ message: error }} onDismiss={() => {}} />}
          {!loading && !error && (
            <div className="module-card module-card-table settings-columns-grid-card">
              <DataGrid
                storageKey={`settings-cols-${list.key}`}
                columns={GRID_COLUMN_EDITOR_COLUMNS}
                rows={rows}
                emptyMessage="Sem colunas nesta lista."
                onRowReorder={handleRowReorder}
                renderCell={(row, col) => {
                  if (col.key === 'visivel') {
                    return (
                      <input
                        type="checkbox"
                        checked={!!row.visivel}
                        onChange={e => setColProp(row.campo, colPatch(row, { visible: e.target.checked }))}
                      />
                    );
                  }
                  if (col.key === 'rotulo') {
                    return (
                      <input
                        type="text"
                        className="data-grid-filter-input"
                        value={row.rotulo || ''}
                        onChange={e => setColProp(row.campo, colPatch(row, { label: e.target.value }))}
                      />
                    );
                  }
                  if (col.key === 'displayPath') {
                    if (!columnHasDisplayPathChoices(row._spCol || {})) {
                      return <span className="form-hint">—</span>;
                    }
                    const pathOptions = displayPathOptionsForColumn(row._spCol || {});
                    if (row.displayPath && !pathOptions.includes(row.displayPath)) {
                      pathOptions.push(row.displayPath);
                    }
                    return (
                      <AppSelect
                        layout="combo"
                        variant="compact"
                        className="data-grid-select"
                        value={row.displayPath || ''}
                        options={pathOptions.map(opt => ({ value: opt, label: displayPathOptionLabel(opt) }))}
                        onChange={v => setColProp(row.campo, colPatch(row, { displayPath: v || null }))}
                        ariaLabel="Propriedade do objeto a mostrar"
                      />
                    );
                  }
                  if (col.key === 'format') {
                    return (
                      <AppSelect
                        layout="combo"
                        variant="compact"
                        className="data-grid-select"
                        value={row.format || ''}
                        options={GRID_COLUMN_FORMAT_OPTIONS}
                        onChange={v => setColProp(row.campo, colPatch(row, { format: v || null }))}
                        ariaLabel="Formato de apresentação na grelha"
                      />
                    );
                  }
                  if (col.key === 'raw') {
                    return <code className="settings-raw-cell" title={row.raw}>{row.raw}</code>;
                  }
                  if (col.key === 'order') {
                    return <span className="form-hint" title="Arraste a linha para alterar a ordem">{row.order}</span>;
                  }
                  if (col.key === 'origem') {
                    return (
                      <span className={`origem-badge ${row._isSystem ? 'system' : 'custom'}`}>
                        {row.origem}
                      </span>
                    );
                  }
                  if (col.key === 'campo') {
                    return <code className="settings-campo-code">{row.campo}</code>;
                  }
                  return undefined;
                }}
              />
            </div>
          )}
          </ModulePageShell>
        </div>
      </div>
    </>
  );
}

function slugifyListName(name) {
  return (name || '').toLowerCase()
    .normalize('NFD').replace(/[\u0300-\u036f]/g, '')
    .replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'list';
}

function defaultSharePointSites(siteUrl = '') {
  return [
    { key: 'habita', name: 'HABITA', siteUrl: siteUrl || '' },
    { key: 'himo', name: 'HIMO', siteUrl: '' }
  ];
}

function mergeSiteListsIntoRegistry(siteLists, registry, siteKey) {
  const others = (registry || []).filter(l => l.siteKey && l.siteKey !== siteKey);
  const thisSiteRegistry = (registry || []).filter(l => !l.siteKey || l.siteKey === siteKey);
  const byId = Object.fromEntries(thisSiteRegistry.filter(l => l.listId).map(l => [l.listId, l]));
  const byName = Object.fromEntries(thisSiteRegistry.map(l => [`${l.siteKey}:${l.displayName}`, l]));

  const merged = siteLists.map(sl => {
    const existing = byId[sl.id] || byName[`${siteKey}:${sl.displayName}`];
    if (existing) {
      return { ...existing, displayName: sl.displayName, listId: sl.id, siteKey };
    }
    const baseKey = slugifyListName(sl.displayName);
    const key = siteKey === 'habita' ? baseKey : `${siteKey}-${baseKey}`;
    return {
      key,
      displayName: sl.displayName,
      listId: sl.id,
      module: 'other',
      siteKey,
      enabled: false
    };
  });

  const orphanThisSite = thisSiteRegistry.filter(r =>
    r.listId && !siteLists.some(sl => sl.id === r.listId));

  return [...others, ...merged, ...orphanThisSite];
}

const ACESSO_PERMISSOES_COLUMNS = [
  { key: 'Colaborador', label: 'Colaborador', visible: true, order: 0, displayPath: 'displayName' },
  { key: 'Fun_x00e7__x00e3_o', label: 'Função', visible: true, order: 1, displayPath: 'value' },
  { key: 'Permiss_x00f5_es', label: 'Permissões', visible: true, order: 2, allowMultiple: true },
  { key: 'Opera_x00e7__x00f5_es', label: 'Operações', visible: true, order: 3, displayPath: 'value', allowMultiple: true },
];

function userPrefsStorageKey(user) {
  const email = (user?.email || user?.Email || 'anon').toLowerCase().replace(/[^a-z0-9@._-]+/g, '-');
  return `myhabita.user.prefs.${email}`;
}

function defaultUserPreferences() {
  return {
    defaultPage: 'dashboard',
    dashboardAno: String(new Date().getFullYear()),
    dashboard: { layoutSavedAt: null, widgets: null },
    notifications: {
      inAppEnabled: true,
      emailEnabled: true,
      avaliacaoAuto: true,
      avaliacaoGestao: true,
      lembretesEmail: true,
    },
  };
}

function normalizeUserPreferences(raw) {
  const d = defaultUserPreferences();
  if (!raw || typeof raw !== 'object') return d;
  return {
    defaultPage: raw.defaultPage || raw.DefaultPage || d.defaultPage,
    dashboardAno: raw.dashboardAno || raw.DashboardAno || d.dashboardAno,
    dashboard: {
      layoutSavedAt: raw.dashboard?.layoutSavedAt || raw.Dashboard?.LayoutSavedAt || null,
      widgets: raw.dashboard?.widgets || raw.Dashboard?.Widgets || null,
    },
    notifications: {
      inAppEnabled: raw.notifications?.inAppEnabled ?? raw.Notifications?.InAppEnabled ?? true,
      emailEnabled: raw.notifications?.emailEnabled ?? raw.Notifications?.EmailEnabled ?? true,
      avaliacaoAuto: raw.notifications?.avaliacaoAuto ?? raw.Notifications?.AvaliacaoAuto ?? true,
      avaliacaoGestao: raw.notifications?.avaliacaoGestao ?? raw.Notifications?.AvaliacaoGestao ?? true,
      lembretesEmail: raw.notifications?.lembretesEmail ?? raw.Notifications?.LembretesEmail ?? true,
    },
  };
}

function loadLocalUserPreferences(user) {
  try {
    const raw = window.localStorage.getItem(userPrefsStorageKey(user));
    return normalizeUserPreferences(raw ? JSON.parse(raw) : null);
  } catch {
    return defaultUserPreferences();
  }
}

function saveLocalUserPreferences(user, prefs) {
  try {
    window.localStorage.setItem(userPrefsStorageKey(user), JSON.stringify(prefs));
  } catch { /* ignore */ }
}

async function fetchUserPreferences(user) {
  try {
    const data = await apiFetch('/api/me/preferences');
    const prefs = normalizeUserPreferences(data);
    saveLocalUserPreferences(user, prefs);
    return prefs;
  } catch {
    return loadLocalUserPreferences(user);
  }
}

async function persistUserPreferences(user, prefs) {
  const normalized = normalizeUserPreferences(prefs);
  saveLocalUserPreferences(user, normalized);
  try {
    const saved = await apiFetch('/api/me/preferences', {
      method: 'PUT',
      body: JSON.stringify(normalized),
    });
    return normalizeUserPreferences(saved);
  } catch {
    return normalized;
  }
}

function snapshotCurrentDashboardLayout(user) {
  const key = user?.email || user?.Email || 'default';
  const layout = loadDashboardLayout(key);
  if (!layout?.widgets?.length) return null;
  return {
    layoutSavedAt: new Date().toISOString(),
    widgets: serializeDashboardWidgets(layout.widgets),
  };
}

function mergeKeptDashboardWidgets(incoming, local) {
  const list = Array.isArray(incoming) ? incoming.map(w => ({ ...w })) : [];
  const ids = new Set(list.map(w => w.id));
  for (const w of local || []) {
    if (w?.id && !ids.has(w.id) && DASHBOARD_WIDGET_DEFS[w.id]) {
      list.push({ ...w });
      ids.add(w.id);
    }
  }
  return list;
}

async function persistDashboardLayout(user, widgets) {
  const key = user?.email || user?.Email || 'default';
  const snap = {
    layoutSavedAt: new Date().toISOString(),
    widgets: serializeDashboardWidgets(widgets),
  };
  saveDashboardLayout(key, { widgets: snap.widgets });
  try {
    window.localStorage.setItem(dashboardLayoutSyncedAtKey(key), snap.layoutSavedAt);
  } catch { /* */ }
  try {
    const current = loadLocalUserPreferences(user);
    await persistUserPreferences(user, { ...current, dashboard: snap });
  } catch { /* best-effort */ }
  return snap;
}

function applyDashboardLayoutFromPrefs(user, prefs) {
  const widgets = prefs?.dashboard?.widgets;
  if (!Array.isArray(widgets) || !widgets.length) return false;
  const key = user?.email || user?.Email || 'default';
  saveDashboardLayout(key, { widgets });
  return true;
}

function ConsultorProfilePage({ user, profileEmail = null, onBack }) {
  const emailHint = profileEmail || user?.email || user?.Email || '';
  const currentYear = new Date().getFullYear();
  const [comissaoAno, setComissaoAno] = useState(() => String(currentYear));
  const [member, setMember] = useState(null);
  const [commission, setCommission] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      if (!emailHint && !(user?.colaboradorId || user?.ColaboradorId)) {
        setLoading(false);
        setError('Sem email de colaborador.');
        return;
      }
      setLoading(true);
      setError(null);
      try {
        const params = new URLSearchParams();
        // Full email or slug (local-part, e.g. jose.alves).
        if (emailHint) params.set('email', emailHint);
        const id = user?.colaboradorId || user?.ColaboradorId;
        if (id && !profileEmail) params.set('id', String(id));
        if (comissaoAno) params.set('ano', comissaoAno);
        const data = await apiFetch(`/api/equipa/membro?${params}`);
        if (cancelled) return;
        setMember(data.member || data.Member || null);
        setCommission(data.commission || data.Commission || null);
      } catch (err) {
        if (!cancelled) {
          setError(err.message || 'Não foi possível carregar o perfil.');
          setMember(null);
          setCommission(null);
        }
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, [emailHint, profileEmail, user?.colaboradorId, user?.ColaboradorId, comissaoAno]);

  const email = memberField(member, 'email', 'Email', 'correioEletronico', 'CorreioEletronico') || emailHint;
  const assinatura = memberAssinatura(member) || userAssinatura(user?.displayName || user?.DisplayName);
  const funcao = memberField(member, 'funcao', 'Funcao') || user?.funcao || user?.Funcao || '—';
  const local = memberField(member, 'local', 'Local') || user?.local || user?.Local || '—';
  const telefone = memberField(member, 'telefone', 'Telefone') || '—';
  const empresa = memberField(member, 'empresa', 'Empresa') || '—';
  const estado = memberField(member, 'estado', 'Estado') || '—';
  const memberMail = memberField(member, 'email', 'Email', 'correioEletronico', 'CorreioEletronico') || email || '—';
  const memberId = member?.id || member?.Id || user?.colaboradorId || user?.ColaboradorId;
  const photoUrl = memberColaboradorImageUrl(
    member?.id || member?.Id ? member : { ...member, id: memberId }
  );
  const admissao = member?.dataAdmissao || member?.DataAdmissao;
  const antiguidade = member?.antiguidade || member?.Antiguidade || '—';
  const admissaoLabel = admissao
    ? new Date(admissao).toLocaleDateString('pt-PT')
    : '—';

  const mergedUser = {
    ...user,
    roles: [], // commission UI is about the profiled consultor, not the viewer
    escalao: commission?.escalao ?? commission?.Escalao ?? user?.escalao,
    objectivo: commission?.objectivo ?? commission?.Objectivo ?? user?.objectivo,
    sumComissao: commission?.sumComissao ?? commission?.SumComissao ?? user?.sumComissao,
    limiteMin: commission?.limiteMin ?? commission?.LimiteMin ?? user?.limiteMin,
    limiteMax: commission?.limiteMax ?? commission?.LimiteMax ?? user?.limiteMax,
    progressPct: commission?.progressPct ?? commission?.ProgressPct ?? user?.progressPct,
    existeComissao: commission?.existeComissao ?? commission?.ExisteComissao ?? user?.existeComissao,
    anoComissao: commission?.ano ?? commission?.anoComissao ?? commission?.Ano ?? Number(comissaoAno),
    planoEscalao: commission?.planoEscalao ?? commission?.PlanoEscalao ?? user?.planoEscalao,
    faixaCount: commission?.faixaCount ?? commission?.FaixaCount ?? user?.faixaCount,
  };
  const metrics = commissionMetrics(mergedUser);
  const hasCommission = !!(mergedUser.existeComissao ?? mergedUser.ExisteComissao);
  const escalao = mergedUser.escalao ?? mergedUser.Escalao ?? null;
  const bandMin = metrics.min;
  const bandMax = metrics.max;
  const faixaPct = Math.min(100, Math.max(0, metrics.segmentPct));
  const planoPct = Math.min(100, Math.max(0, (metrics.filledAmount / Math.max(1, metrics.segmentCount)) * 100));
  const yearLabel = mergedUser.anoComissao || Number(comissaoAno) || currentYear;

  return (
    <>
      <ModuleTopbar
        icon="user"
        title="Perfil do consultor"
        user={user}
        actions={(
          <button type="button" className="btn btn-secondary btn-sm" onClick={onBack}>
            <Icon name="arrowLeft" size={14} />
            Voltar
          </button>
        )}
      />
      <div className="page-body">
        <div className="page-content module-layout perfil-page">
          {loading && <LoadingSpinner message="A carregar perfil..." />}
          {error && !loading && <ErrorBanner error={{ message: error }} onDismiss={() => setError(null)} />}
          {!loading && !error && (
            <div className="perfil-resume">
              <section className="perfil-hero ds-card">
                <div className="perfil-hero-photo">
                  <div className={`perfil-avatar-frame ${hasCommission ? 'has-ring' : ''}`}>
                    {hasCommission && (
                      <CommissionRingSegments
                        filledAmount={metrics.filledAmount}
                        escalao={metrics.planoEscalao ?? escalao}
                        segmentCount={metrics.segmentCount}
                        size={128}
                      />
                    )}
                    <MemberAvatar member={member || { email, assinatura }} size={hasCommission ? 96 : 112} />
                  </div>
                </div>
                <div className="perfil-hero-copy">
                  <p className="perfil-kicker">Currículo HABITA</p>
                  <h2>{assinatura}</h2>
                  <p className="perfil-role">{funcao}</p>
                  <div className="perfil-hero-chips">
                    {local && local !== '—' && <span className="perfil-chip">{local}</span>}
                    {estado && estado !== '—' && <span className="perfil-chip tone-estado">{estado}</span>}
                    {escalao != null && <EscalaoBadge escalao={escalao} />}
                  </div>
                </div>
              </section>

              <div className="perfil-grid">
                <section className="ds-card perfil-card">
                  <h3>Dados profissionais</h3>
                  <dl className="settings-prefs-dl">
                    <div><dt>Email</dt><dd>{memberMail}</dd></div>
                    <div><dt>Telefone</dt><dd>{telefone}</dd></div>
                    <div><dt>Função</dt><dd>{funcao}</dd></div>
                    <div><dt>Local</dt><dd>{local}</dd></div>
                    <div><dt>Empresa / Marca</dt><dd>{empresa}</dd></div>
                    <div><dt>Estado</dt><dd>{estado}</dd></div>
                    <div><dt>Admissão</dt><dd>{admissaoLabel}</dd></div>
                    <div><dt>Antiguidade</dt><dd>{antiguidade}</dd></div>
                    {memberId && <div><dt>ID Colaborador</dt><dd>{memberId}</dd></div>}
                  </dl>
                </section>

                <section className="ds-card perfil-card">
                  <div className="perfil-card-head-row">
                    <h3>Comissão</h3>
                    <AppSelect
                      variant="compact"
                      className="perfil-ano-select"
                      value={comissaoAno}
                      options={[currentYear, currentYear - 1, currentYear - 2, currentYear - 3].map(y => ({ value: String(y), label: String(y) }))}
                      onChange={setComissaoAno}
                      ariaLabel="Ano da comissão"
                    />
                  </div>
                  {hasCommission ? (
                    <>
                      <div className="perfil-commission-hero">
                        <div>
                          <span className="perfil-metric-label">Escalão · {yearLabel}</span>
                          <strong>{escalao != null ? escalao : '—'}</strong>
                        </div>
                        <div>
                          <span className="perfil-metric-label">Comissão aprovada</span>
                          <strong>{formatCurrency(metrics.sum)}</strong>
                        </div>
                        <div>
                          <span className="perfil-metric-label">Na faixa</span>
                          <strong>{Math.round(faixaPct)}%</strong>
                        </div>
                        <div>
                          <span className="perfil-metric-label">No plano</span>
                          <strong>{Math.round(planoPct)}%</strong>
                        </div>
                      </div>
                      <div className="perfil-progress-stack">
                        <div className="perfil-progress-block">
                          <div className="perfil-progress-label-row">
                            <span>Progresso na faixa</span>
                            <strong>{Math.round(faixaPct)}%</strong>
                          </div>
                          <div className="perfil-progress-track" aria-hidden="true">
                            <div className="perfil-progress-fill" style={{ width: `${faixaPct}%` }} />
                          </div>
                          <div className="perfil-progress-ends">
                            <span>{formatCurrency(bandMin)}</span>
                            <span>{bandMax > bandMin ? formatCurrency(bandMax) : '—'}</span>
                          </div>
                        </div>
                        <div className="perfil-progress-block">
                          <div className="perfil-progress-label-row">
                            <span>Progresso no plano</span>
                            <strong>{Math.round(planoPct)}%</strong>
                          </div>
                          <div className="perfil-progress-track" aria-hidden="true">
                            <div className="perfil-progress-fill perfil-progress-fill--plano" style={{ width: `${planoPct}%` }} />
                          </div>
                          <div className="perfil-progress-ends">
                            <span>
                              {escalao != null && metrics.faixaCount > 1
                                ? `Escalão ${escalao}/${metrics.faixaCount}`
                                : escalao != null
                                  ? `Escalão ${escalao}`
                                  : `Plano ${yearLabel}`}
                            </span>
                            <span>
                              {metrics.faixaCount > 1
                                ? `${metrics.faixaCount} faixas`
                                : '—'}
                            </span>
                          </div>
                        </div>
                      </div>
                      <p className="perfil-hint">
                        {escalao != null && metrics.faixaCount > 1
                          ? `Escalão ${escalao}/${metrics.faixaCount}`
                          : escalao != null
                            ? `Escalão ${escalao}`
                            : `Faixa ${yearLabel}`}
                        {metrics.planoEscalao != null && metrics.planoEscalao !== escalao
                          ? ` · plano ${metrics.planoEscalao}`
                          : ''}
                        {bandMax > bandMin
                          ? `: ${formatCurrency(bandMin)} – ${formatCurrency(bandMax)}`
                          : bandMin > 0
                            ? `: ≥ ${formatCurrency(bandMin)}`
                            : ''}
                      </p>
                    </>
                  ) : (
                    <p className="empty-hint">Sem plano comissional para {yearLabel}.</p>
                  )}
                </section>

                <section className="ds-card perfil-card perfil-card-wide">
                  <h3>Contacto rápido</h3>
                  <div className="perfil-actions">
                    {memberMail && memberMail !== '—' && (
                      <a className="btn btn-secondary btn-sm" href={`mailto:${memberMail}`}>
                        <Icon name="envelope" size={14} />
                        Email
                      </a>
                    )}
                    {telefone && telefone !== '—' && (
                      <a className="btn btn-secondary btn-sm" href={`tel:${String(telefone).replace(/\s+/g, '')}`}>
                        <Icon name="phone" size={14} />
                        Telefone
                      </a>
                    )}
                    {photoUrl && (
                      <a className="btn btn-secondary btn-sm" href={photoUrl} target="_blank" rel="noreferrer">
                        <Icon name="camera" size={14} />
                        Foto
                      </a>
                    )}
                  </div>
                </section>
              </div>
            </div>
          )}
        </div>
      </div>
    </>
  );
}

function CacheStatusPanel() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [comparing, setComparing] = useState(false);
  const [error, setError] = useState(null);

  const load = useCallback(async (compare) => {
    if (compare) setComparing(true);
    else setLoading(true);
    setError(null);
    try {
      setData(await apiFetch(`/api/settings/cache${compare ? '?compare=true' : ''}`));
    } catch (err) {
      setError(err.message || 'Não foi possível ler a cache.');
    } finally {
      setLoading(false);
      setComparing(false);
    }
  }, []);

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

  const lists = data?.lists || data?.Lists || [];
  const working = Boolean(data?.working ?? data?.Working);
  const refreshing = Boolean(data?.refreshing ?? data?.Refreshing);
  const connected = Boolean(data?.connected ?? data?.Connected);
  const compared = Boolean(data?.compared ?? data?.Compared);

  const when = (iso) => {
    if (!iso) return 'Ainda sem sincronização';
    const d = new Date(iso);
    if (Number.isNaN(d.getTime())) return '—';
    return d.toLocaleDateString('pt-PT', { day: '2-digit', month: '2-digit' }) + ' ' + formatHmFromDate(d);
  };

  const aheadLabel = (row) => {
    const ahead = row.ahead ?? row.Ahead;
    if (ahead == null) return '—';
    if (ahead === 0) return 'Em dia';
    if (ahead > 0) return `SharePoint +${ahead}`;
    return `App +${Math.abs(ahead)}`;
  };

  return (
    <section className="ds-card cache-status-card">
      <div className="ds-card-head">
        <h3>Cache da app</h3>
        <button type="button" className="btn btn-secondary btn-sm" disabled={comparing} onClick={() => load(true)}>
          {comparing ? 'A comparar…' : 'Comparar com o SharePoint'}
        </button>
      </div>
      {loading ? <CardLoading /> : error ? (
        <p className="empty-hint" style={{ padding: '0 1.125rem 1rem' }}>{error}</p>
      ) : (
        <div className="cache-status-body">
          <p className="cache-status-summary">
            <span className={`cache-status-pill ${working ? 'is-ok' : 'is-off'}`}>{working ? 'A funcionar' : 'Sem cache'}</span>
            <span className={`cache-status-pill ${refreshing ? 'is-busy' : ''}`}>{refreshing ? 'A atualizar dados' : 'Sem atualização em curso'}</span>
            <span className={`cache-status-pill ${connected ? 'is-ok' : 'is-off'}`}>{connected ? 'SharePoint ligado' : 'SharePoint offline'}</span>
          </p>
          <div className="cache-status-table" role="table">
            <div className="cache-status-row is-head" role="row">
              <span>Lista</span>
              <span>Na app</span>
              <span>SharePoint</span>
              <span>Diferença</span>
              <span>Última sincronização</span>
            </div>
            {lists.length === 0 && <p className="empty-hint">Nenhuma lista em cache.</p>}
            {lists.map(row => {
              const key = row.listKey || row.ListKey;
              const count = row.count ?? row.Count ?? 0;
              const sp = row.sharePointCount ?? row.SharePointCount;
              const busy = Boolean(row.refreshing ?? row.Refreshing);
              return (
                <div className="cache-status-row" role="row" key={key}>
                  <span>{key}</span>
                  <span>{busy ? 'A atualizar' : count}</span>
                  <span>{compared ? (sp == null ? '—' : sp) : '—'}</span>
                  <span>{compared ? aheadLabel(row) : '—'}</span>
                  <span>{when(row.lastSyncedUtc || row.LastSyncedUtc)}</span>
                </div>
              );
            })}
          </div>
        </div>
      )}
    </section>
  );
}

const THEME_DEBUG_TABS = [
  { id: 'identidade', label: 'Identidade' },
  { id: 'cores', label: 'Cores' },
  { id: 'tipo', label: 'Tipo e medidas' },
  { id: 'icones', label: 'Ícones e assets' },
  { id: 'componentes', label: 'Componentes' },
  { id: 'tokens', label: 'Tokens CSS' },
];

const THEME_DEBUG_ICON_NAMES = [
  'bank', 'edit', 'graph', 'chartHbar', 'chartPie', 'chartProgress', 'home', 'team', 'trash', 'user',
  'mediacao', 'angariacao', 'soi', 'investimentos', 'eye', 'sliders', 'check', 'settings', 'logout',
  'chevronLeft', 'chevronRight', 'chevronUp', 'chevronDown', 'arrowLeft', 'arrowRight', 'refresh',
  'filter', 'filterClear', 'bars', 'sortAsc', 'sortDesc', 'sortClear', 'search', 'close', 'checkmark',
  'sharepoint', 'document', 'camera', 'message', 'bell', 'calendar', 'external', 'trophy', 'star',
  'ranking', 'kanban', 'plus', 'minus', 'thumb', 'comment', 'share', 'clock', 'activity', 'layout',
  'clipboardCheck', 'clipboardList', 'phone', 'envelope', 'location', 'lock', 'circleXmark',
  'triangleExclamation', 'circleInfo', 'circleQuestion', 'palette',
];

function nexoraTheme() {
  return window.NexoraTheme || window.MyHabitaTheme || {};
}

function copyThemeValue(text, label) {
  const value = String(text || '').trim();
  if (!value) return;
  const done = () => toast.success(`${label} copiado`);
  if (navigator.clipboard?.writeText) {
    navigator.clipboard.writeText(value).then(done).catch(() => toast.error('Não foi possível copiar.'));
    return;
  }
  toast.info(value);
}

function isThemeColorValue(value) {
  const v = String(value || '').trim();
  return /^#([0-9a-f]{3,8})$/i.test(v) || /^rgba?\(/i.test(v) || /^hsla?\(/i.test(v);
}

function ThemeTokenChip({ name, value, hint }) {
  const token = String(name || '');
  const raw = String(value || '');
  const swatchBg = token.startsWith('--') ? `var(${token})` : raw;
  const color = isThemeColorValue(raw) || token.startsWith('--') || raw.startsWith('var(');
  return (
    <AppTooltip label={`Clicar para copiar ${token}`}>
      <button
        type="button"
        className="theme-debug-token"
        onClick={() => copyThemeValue(token.startsWith('--') ? `var(${token})` : raw, token)}
      >
        {color ? <span className="theme-debug-swatch" style={{ background: swatchBg }} aria-hidden="true" /> : null}
        <span className="theme-debug-token-meta">
          <code>{token}</code>
          <span>{raw || '—'}</span>
          {hint ? <small>{hint}</small> : null}
        </span>
      </button>
    </AppTooltip>
  );
}

function ThemeDebugSection({ title, hint, children }) {
  return (
    <section className="theme-debug-section">
      <header className="theme-debug-section-head">
        <h3>{title}</h3>
        {hint ? <p>{hint}</p> : null}
      </header>
      {children}
    </section>
  );
}

const THEME_DEBUG_GRID_COLS = [
  { key: 'referencia', label: 'Referência' },
  { key: 'estado', label: 'Estado', type: 'choice', choices: ['Em curso', 'Concluído'] },
  { key: 'valor', label: 'Valor' },
];

const THEME_DEBUG_GRID_ROWS = [
  { id: 1, referencia: 'T2 Chiado', estado: 'Em curso', valor: '285 000 €' },
  { id: 2, referencia: 'T1 Civilria', estado: 'Concluído', valor: '198 500 €' },
  { id: 3, referencia: 'Loja Baixa', estado: 'Em curso', valor: '412 000 €' },
];

function ThemeDebugGridColMenu() {
  const [op1, setOp1] = useState('equals');
  const [val1, setVal1] = useState('');
  const [logic, setLogic] = useState('and');
  const [op2, setOp2] = useState('contains');
  const [val2, setVal2] = useState('');
  const choiceOpts = [
    { value: 'Em curso', label: 'Em curso' },
    { value: 'Concluído', label: 'Concluído' },
  ];
  const needsValue1 = op1 !== 'isEmpty' && op1 !== 'notEmpty';
  const needsValue2 = op2 !== 'isEmpty' && op2 !== 'notEmpty';
  return (
    <div className="data-grid-col-menu theme-debug-col-menu" role="group" aria-label="Ordenar e filtrar">
      <div className="data-grid-col-menu-section">
        <button type="button" className="data-grid-col-menu-item">
          <Icon name="sortAsc" size={14} /> Ordenar A → Z
        </button>
        <button type="button" className="data-grid-col-menu-item">
          <Icon name="sortDesc" size={14} /> Ordenar Z → A
        </button>
        <button type="button" className="data-grid-col-menu-item" disabled>
          <Icon name="sortClear" size={14} /> Remover ordenação
        </button>
      </div>
      <div className="data-grid-col-menu-section">
        <div className="data-grid-col-menu-title">Mostrar linhas onde:</div>
        <AppSelect
          layout="list"
          variant="compact"
          showMenuTitle={false}
          value={op1}
          options={GRID_FILTER_OPS}
          onChange={setOp1}
          ariaLabel="Primeira condição"
        />
        {needsValue1 && (
          <AppSelect
            layout="list"
            variant="compact"
            showMenuTitle={false}
            value={val1}
            emptyOption="(qualquer)"
            options={choiceOpts}
            onChange={setVal1}
            ariaLabel="Valor do filtro"
          />
        )}
        <AppSelect
          layout="list"
          variant="compact"
          showMenuTitle={false}
          className="data-grid-menu-logic"
          value={logic}
          options={[{ value: 'and', label: 'e' }, { value: 'or', label: 'ou' }]}
          onChange={setLogic}
          ariaLabel="Lógica"
        />
        <AppSelect
          layout="list"
          variant="compact"
          showMenuTitle={false}
          value={op2}
          options={GRID_FILTER_OPS}
          onChange={setOp2}
          ariaLabel="Segunda condição"
        />
        {needsValue2 && (
          <input
            className="data-grid-menu-input"
            type="text"
            value={val2}
            placeholder="Valor…"
            onChange={e => setVal2(e.target.value)}
            aria-label="Segundo valor do filtro"
          />
        )}
        <div className="data-grid-col-menu-actions">
          <button type="button" className="btn btn-primary btn-sm">Filtrar</button>
          <button type="button" className="btn btn-secondary btn-sm">Limpar</button>
        </div>
      </div>
    </div>
  );
}

function ThemeDebugPage({ user, onBack }) {
  const theme = nexoraTheme();
  const [tab, setTab] = useState('identidade');
  const [query, setQuery] = useState('');
  const [filterOpen, setFilterOpen] = useState(false);
  const [confirmOpen, setConfirmOpen] = useState(false);
  const [cssVars, setCssVars] = useState([]);
  const [natureza, setNatureza] = useState('venda');
  const [prob, setProb] = useState('alta');
  const [mes, setMes] = useState('10');
  const [ops, setOps] = useState(['lisboa']);
  const [comboVal, setComboVal] = useState('item1');
  const [walletSeg, setWalletSeg] = useState('recebido');
  const [chipNat, setChipNat] = useState('Venda');
  const [dateVal, setDateVal] = useState('2026-10-01');
  const [timeVal, setTimeVal] = useState('09:30');
  const [emailVal, setEmailVal] = useState('nome@habita.pt');
  const [phoneVal, setPhoneVal] = useState('912345678');
  const [moneyVal, setMoneyVal] = useState(128400);
  const [listView, setListView] = useState('cards');
  const [prefOn, setPrefOn] = useState(true);
  const [chipOn, setChipOn] = useState(true);
  const q = query.trim().toLowerCase();
  const match = useCallback((...parts) => {
    if (!q) return true;
    return parts.some(p => String(p || '').toLowerCase().includes(q));
  }, [q]);

  useEffect(() => {
    const styles = getComputedStyle(document.documentElement);
    const names = new Set();
    const collect = (style) => {
      if (!style) return;
      for (let i = 0; i < style.length; i += 1) {
        const name = style[i];
        if (name && name.startsWith('--')) names.add(name);
      }
    };
    collect(document.documentElement.style);
    try {
      for (const sheet of document.styleSheets) {
        let rules;
        try { rules = sheet.cssRules; } catch { continue; }
        if (!rules) continue;
        for (const rule of rules) {
          if (rule.selectorText === ':root') collect(rule.style);
        }
      }
    } catch { /* ignore */ }
    const rows = [...names]
      .sort((a, b) => a.localeCompare(b))
      .map(name => ({ name, value: (styles.getPropertyValue(name) || '').trim() }));
    setCssVars(rows);
  }, []);

  if (!userIsAdmin(user)) {
    return (
      <>
        <ModuleTopbar icon="palette" title="Tema Nexora" user={user} actions={(
          <button type="button" className="btn btn-secondary" onClick={onBack}>Voltar</button>
        )} />
        <div className="page-body">
          <div className="page-content module-layout">
            <div className="module-placeholder">
              <span className="module-placeholder-icon" aria-hidden="true"><Icon name="lock" size={48} /></span>
              <h2>Acesso restrito</h2>
              <p>O catálogo do tema Nexora está disponível apenas para administradores.</p>
            </div>
          </div>
        </div>
      </>
    );
  }

  const colors = theme.colors || {};
  const fonts = theme.fonts || {};
  const spacing = theme.spacing || {};
  const shadows = theme.shadows || {};
  const assets = theme.assets || {};
  const cssVal = (name, fallback) => cssVars.find(row => row.name === name)?.value || fallback || '';
  const colorGroups = [
    {
      title: 'Marca',
      hint: 'Primárias do Manual de Normas. Usar estas — não inventar hex avulso.',
      items: [
        { name: 'primary1 / brandNavy', css: '--primary-1', value: colors.primary1, hint: 'Navy profundo, fundo da app' },
        { name: 'primary2 / brandGreen / accent', css: '--primary-2', value: colors.primary2, hint: 'Verde, acções primárias' },
        { name: 'primary3', css: '--primary-3', value: colors.primary3, hint: 'Navy cartão / #1F2C5C' },
        { name: 'primary4', css: '--primary-4', value: colors.primary4, hint: 'Verde claro hover' },
        { name: 'accentMuted', css: '--accent-muted', value: colors.accentMuted, hint: 'Fundo verde suave' },
        { name: 'loginPatternH', css: '--login-pattern-h', value: colors.loginPatternH, hint: 'Padrão H do fundo' },
      ],
    },
    {
      title: 'Semânticas',
      items: [
        { name: 'success', css: '--color-success', value: colors.success },
        { name: 'error', css: '--color-error', value: colors.error },
        { name: 'info', css: '--color-info', value: colors.info },
        { name: 'warning', css: '--color-warning', value: colors.warning },
      ],
    },
    {
      title: 'Superfície e texto',
      items: [
        { name: 'surface / white', css: '--ds-surface', value: colors.surface },
        { name: 'surfaceMuted', css: '--ds-surface-muted', value: colors.surfaceMuted },
        { name: 'border', css: '--ds-border', value: colors.border },
        { name: 'text', css: '--text', value: colors.text },
        { name: 'textMuted', css: '--ds-text-muted', value: colors.textMuted },
        { name: 'black', css: '--color-black', value: colors.black },
      ],
    },
    {
      title: 'Tons de estado e natureza',
      hint: 'Uma paleta para badges em toda a app (estado + natureza).',
      items: [
        { name: 'Em curso', css: '--tone-em-curso-bg', value: cssVal('--tone-em-curso-bg'), hint: 'texto --tone-em-curso-fg' },
        { name: 'Concluído', css: '--tone-concluido-bg', value: cssVal('--tone-concluido-bg'), hint: 'texto --tone-concluido-fg' },
        { name: 'Perdido', css: '--tone-perdido-bg', value: cssVal('--tone-perdido-bg'), hint: 'texto --tone-perdido-fg' },
        { name: 'Venda', css: '--tone-venda-bg', value: cssVal('--tone-venda-bg'), hint: 'texto --tone-venda-fg' },
        { name: 'Arrendamento', css: '--tone-arrendamento-bg', value: cssVal('--tone-arrendamento-bg'), hint: 'texto --tone-arrendamento-fg' },
        { name: 'Aprovado', css: '--tone-aprovado-bg', value: cssVal('--tone-aprovado-bg'), hint: 'texto --tone-aprovado-fg' },
      ],
    },
    {
      title: 'Aliases',
      hint: 'Nomes antigos no CSS — apontam para as mesmas cores do tema.',
      items: [
        { name: 'navy', css: '--navy', value: cssVal('--navy', colors.brandNavy) },
        { name: 'navy-deep', css: '--navy-deep', value: cssVal('--navy-deep', colors.primary3) },
        { name: 'navy-light', css: '--navy-light', value: cssVal('--navy-light', '#252458') },
        { name: 'green', css: '--green', value: cssVal('--green', colors.brandGreen) },
        { name: 'green-dark', css: '--green-dark', value: cssVal('--green-dark', '#268a3e') },
        { name: 'gray-100', css: '--gray-100', value: cssVal('--gray-100', colors.surfaceMuted) },
        { name: 'gray-200', css: '--gray-200', value: cssVal('--gray-200', colors.border) },
        { name: 'gray-300', css: '--gray-300', value: cssVal('--gray-300', '#d1d9e6') },
        { name: 'gray-400', css: '--gray-400', value: cssVal('--gray-400', '#94a3b8') },
        { name: 'gray-600', css: '--gray-600', value: cssVal('--gray-600', colors.textMuted) },
      ],
    },
  ];

  const typeItems = [
    { name: 'font-heading', value: fonts.heading, hint: 'Roboto — títulos, KPIs, botão primário' },
    { name: 'font-body', value: fonts.body, hint: 'Roboto — corpo e controlos' },
    { name: 'font-size-tiny', value: fonts.size?.tiny },
    { name: 'font-size-regular', value: fonts.size?.regular },
    { name: 'font-size-subtitle', value: fonts.size?.subtitle },
    { name: 'font-size-title', value: fonts.size?.title },
    { name: 'font-size-huge', value: fonts.size?.huge },
  ];
  const measureItems = [
    ...Object.entries(spacing).map(([k, v]) => ({ name: k, value: v })),
    ...Object.entries(shadows).map(([k, v]) => ({ name: `shadow.${k}`, value: v })),
  ];
  const visibleVars = cssVars.filter(row => match(row.name, row.value));
  const visibleIcons = THEME_DEBUG_ICON_NAMES.filter(name => match(name));
  const visibleAssets = Object.entries(assets).filter(([k, v]) => match(k, v));

  return (
    <>
      <ModuleTopbar
        icon="palette"
        title="Tema Nexora"
        user={user}
        actions={(
          <button type="button" className="btn btn-secondary" onClick={onBack}>Voltar</button>
        )}
      />
      <div className="page-body">
        <div className="page-content module-layout">
          <ModulePageShell
            className="theme-debug-shell"
            tabs={(
              <div className="tabs tabs-segmented" role="tablist" aria-label="Secções do tema Nexora">
                {THEME_DEBUG_TABS.map(t => (
                  <button
                    key={t.id}
                    type="button"
                    className={`tab${tab === t.id ? ' active' : ''}`}
                    role="tab"
                    aria-selected={tab === t.id}
                    onClick={() => setTab(t.id)}
                  >
                    {t.label}
                  </button>
                ))}
              </div>
            )}
            toolbar={(
              <div className="theme-debug-toolbar">
                <label className="topbar-search theme-debug-search">
                  <Icon name="search" size={16} />
                  <input
                    type="search"
                    value={query}
                    onChange={e => setQuery(e.target.value)}
                    placeholder="Filtrar token, cor, ícone…"
                    aria-label="Filtrar catálogo do tema"
                  />
                </label>
                <span className="theme-debug-toolbar-meta">
                  {theme.name || 'Nexora'} · {theme.id || 'nexora'}
                </span>
              </div>
            )}
          >
            <div className="theme-debug-body">
              {tab === 'identidade' && (
                <>
                  <ThemeDebugSection
                    title="Nome do tema"
                    hint="Use este nome ao criar elementos novos. Não invente um segundo tema."
                  >
                    <dl className="settings-prefs-dl theme-debug-identity">
                      <div><dt>Nome</dt><dd><strong>{theme.name || 'Nexora'}</strong></dd></div>
                      <div><dt>Nome completo</dt><dd>{theme.fullName || 'Nexora'}</dd></div>
                      <div><dt>ID</dt><dd><code>{theme.id || 'nexora'}</code></dd></div>
                      <div>
                        <dt>Fonte</dt>
                        <dd>
                          <AppTooltip label="Abrir o PDF do Manual de Normas HABITA">
                            <a
                              className="theme-debug-manual-link"
                              href={assets.manualNormas || '/docs/manual-de-normas-habita.pdf'}
                              target="_blank"
                              rel="noopener noreferrer"
                            >
                              {theme.source || 'Manual de Normas HABITA'}
                            </a>
                          </AppTooltip>
                        </dd>
                      </div>
                      <div><dt>Tipografia</dt><dd>Roboto — Light, Regular, Medium, Bold</dd></div>
                      <div><dt>JS</dt><dd><code>window.NexoraTheme</code> / <code>window.MyHabitaTheme</code></dd></div>
                      <div><dt>Página</dt><dd><code>/theme</code></dd></div>
                    </dl>
                  </ThemeDebugSection>
                  <ThemeDebugSection title="Como referenciar">
                    <ul className="theme-debug-list">
                      <li>Dropdown: <code>{'<AppSelect … />'}</code> — chips no popover</li>
                      <li>Combobox: <code>{'<AppSelect layout="combo" … />'}</code> — escreva para filtrar</li>
                      <li>Cores JS: <code>window.NexoraTheme.colors.primary2</code></li>
                      <li>Cores CSS: <code>var(--accent)</code>, <code>var(--primary-1)</code>, <code>var(--primary-3)</code></li>
                      <li>Ícone: <code>{'<Icon name="graph" />'}</code></li>
                      <li>Tooltip: <code>{'<AppTooltip label="…">'}</code> — nunca o atributo HTML <code>title</code></li>
                      <li>Grelha: <code>{'<DataGrid … />'}</code> — ordenar/filtrar no cabeçalho (<code>data-grid-col-menu</code>)</li>
                      <li>Cartão: <code>ds-card</code> · Botão: <code>btn btn-primary</code> · Badge: <code>status-badge</code></li>
                    </ul>
                  </ThemeDebugSection>
                </>
              )}

              {tab === 'cores' && colorGroups.map(group => {
                const items = group.items.filter(item => match(item.name, item.css, item.value, item.hint));
                if (!items.length) return null;
                return (
                  <ThemeDebugSection key={group.title} title={group.title} hint={group.hint}>
                    <div className="theme-debug-token-grid">
                      {items.map(item => (
                        <ThemeTokenChip
                          key={item.css}
                          name={item.css}
                          value={item.value}
                          hint={item.hint || item.name}
                        />
                      ))}
                    </div>
                    {group.title === 'Tons de estado e natureza' && (
                      <div className="theme-debug-inline theme-debug-inline-follow">
                        <span className="status-badge status-em-curso">Em curso</span>
                        <span className="status-badge status-concluido">Concluído</span>
                        <span className="status-badge status-perdido">Perdido</span>
                        <span className="status-badge status-venda">Venda</span>
                        <span className="status-badge status-arrendamento">Arrendamento</span>
                        <span className="status-badge status-aprovado">Aprovado</span>
                      </div>
                    )}
                  </ThemeDebugSection>
                );
              })}

              {tab === 'tipo' && (
                <>
                  <ThemeDebugSection title="Tipografia" hint="Roboto nos títulos e no corpo — Manual de Normas HABITA.">
                    <div className="theme-debug-type-samples">
                      <p className="theme-debug-type-huge">Título huge 28px</p>
                      <p className="theme-debug-type-title">Título 20px</p>
                      <p className="theme-debug-type-subtitle">Subtítulo / corpo 14px — o texto corrente da app.</p>
                      <p className="theme-debug-type-regular">Regular 12px — tabelas e meta.</p>
                      <p className="theme-debug-type-tiny">TINY 10px — labels e badges.</p>
                    </div>
                    <div className="theme-debug-token-grid">
                      {typeItems.filter(item => match(item.name, item.value, item.hint)).map(item => (
                        <ThemeTokenChip key={item.name} name={item.name} value={item.value} hint={item.hint} />
                      ))}
                    </div>
                  </ThemeDebugSection>
                  <ThemeDebugSection title="Espaçamento, raios e sombras">
                    <div className="theme-debug-token-grid">
                      {measureItems.filter(item => match(item.name, item.value)).map(item => (
                        <ThemeTokenChip key={item.name} name={item.name} value={item.value} />
                      ))}
                    </div>
                    <div className="theme-debug-shadow-row">
                      <div className="theme-debug-shadow-card" style={{ boxShadow: 'var(--shadow-card)' }}>shadow-card</div>
                      <div className="theme-debug-shadow-card is-shell" style={{ boxShadow: 'var(--shadow-shell)' }}>shadow-shell</div>
                    </div>
                  </ThemeDebugSection>
                </>
              )}

              {tab === 'icones' && (
                <>
                  <ThemeDebugSection title="Ícones" hint="Reutilize um nome já no mapa Icon — não adicione SVG avulso.">
                    <div className="theme-debug-icon-grid">
                      {visibleIcons.map(name => (
                        <AppTooltip key={name} label={`Clicar para copiar ${name}`}>
                          <button
                            type="button"
                            className="theme-debug-icon-cell"
                            onClick={() => copyThemeValue(name, name)}
                          >
                            <Icon name={name} size={20} />
                            <code>{name}</code>
                          </button>
                        </AppTooltip>
                      ))}
                    </div>
                    {!visibleIcons.length && <p className="form-hint">Nenhum ícone corresponde ao filtro.</p>}
                  </ThemeDebugSection>
                  <ThemeDebugSection title="Assets">
                    <div className="theme-debug-asset-grid">
                      {visibleAssets.map(([key, src]) => (
                        <AppTooltip key={key} label={`Clicar para copiar ${src}`}>
                          <button
                            type="button"
                            className="theme-debug-asset"
                            onClick={() => copyThemeValue(src, key)}
                          >
                            {/\.(png|jpe?g|gif|svg)$/i.test(src)
                              ? <img src={src} alt="" />
                              : <Icon name="document" size={28} />}
                            <span>
                              <code>{key}</code>
                              <small>{src}</small>
                            </span>
                          </button>
                        </AppTooltip>
                      ))}
                    </div>
                  </ThemeDebugSection>
                </>
              )}

              {tab === 'componentes' && (
                <>
                  <ThemeDebugSection
                    title="Dropdown"
                    hint="AppSelect — chips no popover. Variantes field, pill, compact e multiple."
                  >
                    <div className="theme-debug-form">
                      <div className="form-group">
                        <label>Lista (field)</label>
                        <AppSelect
                          value={natureza}
                          options={[
                            { value: 'venda', label: 'Venda' },
                            { value: 'arrendamento', label: 'Arrendamento' },
                          ]}
                          onChange={setNatureza}
                          ariaLabel="Lista"
                        />
                      </div>
                      <div className="form-group">
                        <label>Probabilidade</label>
                        <AppSelect
                          value={prob}
                          options={['Alta', 'Média', 'Baixa'].map(o => ({ value: o.toLowerCase(), label: o }))}
                          onChange={setProb}
                          ariaLabel="Probabilidade"
                        />
                      </div>
                      <div className="form-group">
                        <label>Múltipla</label>
                        <AppSelect
                          multiple
                          placeholder="Operações"
                          value={ops}
                          options={[
                            { value: 'lisboa', label: 'Lisboa' },
                            { value: 'porto', label: 'Porto' },
                            { value: 'algarve', label: 'Algarve' },
                          ]}
                          onChange={setOps}
                          ariaLabel="Operações"
                        />
                      </div>
                    </div>
                    <div className="theme-debug-inline theme-debug-inline-follow">
                      <AppSelect
                        variant="pill"
                        label="Mês"
                        value={mes}
                        options={[{ value: '', label: 'Todos' }, ...['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'].map((m, i) => ({ value: String(i + 1), label: m }))]}
                        onChange={setMes}
                        ariaLabel="Mês"
                      />
                      <AppSelect
                        variant="compact"
                        value={prob}
                        options={['Alta', 'Média', 'Baixa'].map(o => ({ value: o.toLowerCase(), label: o }))}
                        onChange={setProb}
                        ariaLabel="Compacto"
                        showMenuTitle={false}
                      />
                      <AppSelect
                        variant="compact"
                        value="venda"
                        disabled
                        options={[{ value: 'venda', label: 'Venda' }]}
                        onChange={() => {}}
                        ariaLabel="Desativado"
                      />
                    </div>
                  </ThemeDebugSection>
                  <ThemeDebugSection
                    title="Combobox"
                    hint={'AppSelect layout="combo" — escreva para filtrar. onQuery para pesquisa remota (como o consultor).'}
                  >
                    <div className="theme-debug-form theme-debug-combo-demo">
                      <AppSelect
                        layout="combo"
                        label="Label"
                        value={comboVal}
                        placeholder="Pesquisar…"
                        options={[
                          { value: 'item1', label: 'Item 1' },
                          { value: 'item2', label: 'Item 2' },
                          { value: 'item3', label: 'Item 3' },
                          { value: 'lisboa', label: 'Lisboa' },
                          { value: 'porto', label: 'Porto' },
                          { value: 'algarve', label: 'Algarve' },
                          { value: 'coimbra', label: 'Coimbra' },
                        ]}
                        onChange={setComboVal}
                        ariaLabel="Label"
                      />
                    </div>
                  </ThemeDebugSection>
                  <ThemeDebugSection title="Botões" hint="Classes: btn + btn-primary | btn-secondary | btn-danger | btn-sm.">
                    <div className="theme-debug-inline">
                      <button type="button" className="btn btn-primary">Primário</button>
                      <button type="button" className="btn btn-secondary">Secundário</button>
                      <button type="button" className="btn btn-danger">Perigo</button>
                      <button type="button" className="btn btn-primary btn-sm">Primário sm</button>
                      <button type="button" className="btn btn-secondary" disabled>Desativado</button>
                      <button type="button" className="btn-primary-accent">
                        <Icon name="plus" size={16} color="#FFFFFF" />
                        <span className="btn-label-text">Inserir</span>
                      </button>
                      <button
                        type="button"
                        className={`btn-filter-toggle${filterOpen ? ' active' : ''}`}
                        aria-label="Filtros"
                        aria-pressed={filterOpen}
                        onClick={() => setFilterOpen(v => !v)}
                      >
                        <Icon name="filter" size={18} />
                      </button>
                      <button type="button" className="btn-icon-ghost" aria-label="Fechar exemplo">
                        <Icon name="close" size={16} />
                      </button>
                      <PhoneListViewToggle view={listView} onChange={setListView} />
                    </div>
                  </ThemeDebugSection>
                  <ThemeDebugSection title="Tooltip" hint="AppTooltip — superfície branca, texto navy, bordo verde à esquerda.">
                    <div className="theme-debug-inline">
                      <AppTooltip label="Em curso 429 522,70 € · Concluído 518 295 €">
                        <button type="button" className="btn btn-secondary">Passar o rato</button>
                      </AppTooltip>
                    </div>
                  </ThemeDebugSection>
                  <ThemeDebugSection title="Data e hora" hint="DatePickerField e TimePickerField — sem o date nativo do browser.">
                    <div className="theme-debug-form">
                      <div className="form-group">
                        <label>Data</label>
                        <DatePickerField value={dateVal} onChange={setDateVal} />
                      </div>
                      <div className="form-group">
                        <label>Hora</label>
                        <TimePickerField value={timeVal} onChange={setTimeVal} />
                      </div>
                    </div>
                  </ThemeDebugSection>
                  <ThemeDebugSection title="Campos de formulário">
                    <div className="theme-debug-form">
                      <div className="form-group">
                        <label>Texto</label>
                        <input type="text" defaultValue="Negócio exemplo" />
                      </div>
                      <div className="form-group">
                        <label>Pesquisa</label>
                        <input type="search" placeholder="Pesquisar…" />
                      </div>
                      <div className="form-group">
                        <label>Email</label>
                        <EmailInputField value={emailVal} onChange={setEmailVal} label="Email" />
                      </div>
                      <div className="form-group">
                        <label>Contacto</label>
                        <ContactoInputField value={phoneVal} onChange={setPhoneVal} label="Contacto" />
                      </div>
                      <div className="form-group">
                        <label>Valor</label>
                        <NumericInputField value={moneyVal} onChange={setMoneyVal} kind="money" label="Valor" />
                      </div>
                      <div className="form-group">
                        <label>Notas</label>
                        <textarea rows={2} defaultValue="Observações do negócio." />
                      </div>
                    </div>
                  </ThemeDebugSection>
                  <ThemeDebugSection title="Chips de escolha" hint="ChoiceChipField / dash-prob-chip — opções curtas em formulários.">
                    <ChoiceChipField
                      label="Natureza"
                      options={['Venda', 'Arrendamento']}
                      value={chipNat}
                      onChange={setChipNat}
                    />
                  </ThemeDebugSection>
                  <ThemeDebugSection title="Checkboxes">
                    <div className="theme-debug-checks">
                      <label className="settings-check">
                        <input type="checkbox" checked={prefOn} onChange={e => setPrefOn(e.target.checked)} />
                        Notificações por email
                      </label>
                      <label className={`avaliacao-check${chipOn ? ' is-on' : ''}`}>
                        <input
                          type="checkbox"
                          checked={chipOn}
                          onChange={e => setChipOn(e.target.checked)}
                        />
                        Em curso
                      </label>
                    </div>
                  </ThemeDebugSection>
                  <ThemeDebugSection title="Badges e chips">
                    <div className="theme-debug-inline">
                      <span className="status-badge status-em-curso">Em curso</span>
                      <span className="status-badge status-concluido">Concluído</span>
                      <span className="status-badge status-perdido">Perdido</span>
                      <span className="status-badge status-venda">Venda</span>
                      <span className="status-badge status-arrendamento">Arrendamento</span>
                      <span className="status-badge status-aprovado">Aprovado</span>
                      <button type="button" className="dash-prob-chip">Chip</button>
                      <button type="button" className="dash-prob-chip active">Chip ativo</button>
                      <span className="theme-debug-escalao-demo" aria-label="Escalão 3">
                        <span className="user-escalao-chip">E3</span>
                      </span>
                      <span className="theme-debug-on-navy">
                        <span className="connection-badge live">Live</span>
                      </span>
                      <div className="notif-bell-wrap">
                        <AppTooltip label="3 notificações">
                          <button type="button" className="btn-icon-ghost notif-bell-btn" aria-label="Notificações">
                            <Icon name="bell" size={18} />
                            <span className="notif-bell-badge">3</span>
                          </button>
                        </AppTooltip>
                      </div>
                    </div>
                  </ThemeDebugSection>
                  <ThemeDebugSection title="Separadores">
                    <div className="theme-debug-tabs-demo">
                      <div className="tabs tabs-segmented" role="tablist">
                        <button type="button" className="tab active">Negócios</button>
                        <button type="button" className="tab">Faturação</button>
                        <button type="button" className="tab">Comissão</button>
                      </div>
                      <div className="tabs" role="tablist">
                        <button type="button" className="tab active">Perfil</button>
                        <button type="button" className="tab">Dashboard</button>
                        <button type="button" className="tab">Notificações</button>
                      </div>
                    </div>
                  </ThemeDebugSection>
                  <ThemeDebugSection title="Cartões">
                    <div className="theme-debug-cards-row">
                      <div className="ds-card theme-debug-demo-card">
                        <div className="ds-card-head">
                          <h3><Icon name="graph" size={16} /> Pipeline</h3>
                        </div>
                        <p className="form-hint theme-debug-card-hint">
                          Cartão padrão. Reutilize <code>ds-card</code> + <code>ds-card-head</code>.
                        </p>
                      </div>
                      <div className="ds-card kpi-card-v2 theme-debug-demo-card">
                        <div className="kpi-card-v2-head">
                          <span className="kpi-card-v2-label">Valor total faturação</span>
                        </div>
                        <div className="kpi-card-v2-body">
                          <div className="kpi-card-v2-value">128 400 €</div>
                          <p className="kpi-trend positive">+12% vs mês anterior</p>
                        </div>
                      </div>
                    </div>
                  </ThemeDebugSection>
                  <ThemeDebugSection
                    title="Barra de progresso"
                    hint="As barras da app: dash-bar-track, carteira selecionável, mix, perfil e SOI. Navy = em curso / venda; verde = concluído / arrendamento."
                  >
                    <div className="theme-debug-bars-gallery">
                      <div>
                        <p className="theme-debug-kicker">dash-bar-row — composta (Previsão Comissão)</p>
                        <div className="dash-bars theme-debug-bars">
                          <div className="dash-bar-row">
                            <span className="dash-bar-label">Lisboa</span>
                            <AppTooltip label="Em curso 429 522,70 € · Concluído 518 295 €">
                              <div className="dash-bar-track">
                                <div className="dash-bar-fill is-pipe" style={{ width: '45%' }} />
                                <div className="dash-bar-fill is-won" style={{ width: '55%' }} />
                              </div>
                            </AppTooltip>
                            <strong className="dash-bar-value">947 818 €</strong>
                          </div>
                          <div className="dash-bar-row">
                            <span className="dash-bar-label">Porto</span>
                            <AppTooltip label="Em curso 210 000 € · Concluído 90 000 €">
                              <div className="dash-bar-track">
                                <div className="dash-bar-fill is-pipe" style={{ width: '70%' }} />
                                <div className="dash-bar-fill is-won" style={{ width: '30%' }} />
                              </div>
                            </AppTooltip>
                            <strong className="dash-bar-value">300 000 €</strong>
                          </div>
                          <div className="dash-bar-legend">
                            <span><i className="dash-swatch is-pipe" /> Em curso</span>
                            <span><i className="dash-swatch is-won" /> Concluído</span>
                          </div>
                        </div>
                      </div>

                      <div>
                        <p className="theme-debug-kicker">dash-bar-row — simples (negócios / operação)</p>
                        <div className="dash-bars theme-debug-bars">
                          <div className="dash-bar-row">
                            <span className="dash-bar-label">Lisboa</span>
                            <div className="dash-bar-track">
                              <div className="dash-bar-fill" style={{ width: '82%' }} />
                            </div>
                            <strong className="dash-bar-value">24</strong>
                          </div>
                          <div className="dash-bar-row">
                            <span className="dash-bar-label">Porto</span>
                            <div className="dash-bar-track">
                              <div className="dash-bar-fill" style={{ width: '54%' }} />
                            </div>
                            <strong className="dash-bar-value">16</strong>
                          </div>
                          <div className="dash-bar-row">
                            <span className="dash-bar-label">Algarve</span>
                            <div className="dash-bar-track">
                              <div className="dash-bar-fill" style={{ width: '28%' }} />
                            </div>
                            <strong className="dash-bar-value">8</strong>
                          </div>
                        </div>
                      </div>

                      <div>
                        <p className="theme-debug-kicker">dash-bar-pair — venda / arrendamento</p>
                        <div className="dash-bars theme-debug-bars">
                          <div className="dash-bar-row">
                            <span className="dash-bar-label">Lisboa</span>
                            <div className="dash-bar-pair">
                              <AppTooltip label="Venda 640 000 €">
                                <div className="dash-bar-track">
                                  <div className="dash-bar-fill is-venda" style={{ width: '80%' }} />
                                </div>
                              </AppTooltip>
                              <AppTooltip label="Arrendamento 160 000 €">
                                <div className="dash-bar-track">
                                  <div className="dash-bar-fill is-arrend" style={{ width: '20%' }} />
                                </div>
                              </AppTooltip>
                            </div>
                            <strong className="dash-bar-value">800 000 €</strong>
                          </div>
                          <div className="dash-bar-legend">
                            <span><i className="dash-swatch is-venda" /> Venda</span>
                            <span><i className="dash-swatch is-arrend" /> Arrendamento</span>
                          </div>
                        </div>
                      </div>

                      <div>
                        <p className="theme-debug-kicker">comissao-wallet-track — selecionável (carteira). Clique num segmento.</p>
                        <div className="comissao-wallet-mix theme-debug-wallet-mix">
                          <div className="comissao-wallet-mix-row">
                            <span className="comissao-wallet-mix-label">Consultor</span>
                            <AppTooltip label="Recebido 18 400 € · A receber 11 200 €">
                              <div
                                className="comissao-wallet-track"
                                role="img"
                                aria-label="Recebido 18 400 euros, a receber 11 200 euros"
                              >
                                <WalletMixSeg
                                  pct={62}
                                  tone="green"
                                  active={walletSeg === 'recebido'}
                                  label="Recebido 18 400 €"
                                  showLabel
                                  onToggle={() => setWalletSeg(s => s === 'recebido' ? null : 'recebido')}
                                />
                                <WalletMixSeg
                                  pct={38}
                                  tone="navy"
                                  active={walletSeg === 'aReceber'}
                                  label="A receber 11 200 €"
                                  showLabel
                                  onToggle={() => setWalletSeg(s => s === 'aReceber' ? null : 'aReceber')}
                                />
                              </div>
                            </AppTooltip>
                            <strong className="comissao-wallet-mix-total">29 600 €</strong>
                          </div>
                          <div className="comissao-wallet-bar-legend">
                            <span><i className="dash-swatch is-won" /> Recebido <em>18 400 €</em></span>
                            <span><i className="dash-swatch is-pipe" /> A receber <em>11 200 €</em></span>
                          </div>
                        </div>
                      </div>

                      <div>
                        <p className="theme-debug-kicker">mix-tile-track — Mix Vendas / Arrendamento</p>
                        <div className="mix-compare theme-debug-mix">
                          <div className="mix-compare-head" role="row">
                            <span />
                            <span className="is-venda"><i className="dash-swatch is-venda" />Venda</span>
                            <span className="is-arrend"><i className="dash-swatch is-arrend" />Arrend.</span>
                          </div>
                          <AppTooltip label="2026 · Venda 640 000 € · Arrendamento 160 000 €" className="mix-compare-tip">
                            <div className="mix-compare-row" role="row">
                              <span className="mix-vbar-label">2026</span>
                              <strong className="is-venda">640 mil €</strong>
                              <strong className="is-arrend">160 mil €</strong>
                              <div className="mix-tile-track" aria-hidden="true">
                                <span className="mix-row-fill is-venda" style={{ width: '80%' }} />
                                <span className="mix-row-fill is-arrend" style={{ width: '20%' }} />
                              </div>
                            </div>
                          </AppTooltip>
                        </div>
                      </div>

                      <div>
                        <p className="theme-debug-kicker">perfil-progress-track — escalão / faixa</p>
                        <div className="perfil-progress-stack theme-debug-perfil-progress">
                          <div className="perfil-progress-block">
                            <div className="perfil-progress-label-row">
                              <span>Faixa atual</span>
                              <strong>62%</strong>
                            </div>
                            <div className="perfil-progress-track" aria-hidden="true">
                              <div className="perfil-progress-fill" style={{ width: '62%' }} />
                            </div>
                            <div className="perfil-progress-ends">
                              <span>0 €</span>
                              <span>40 000 €</span>
                            </div>
                          </div>
                          <div className="perfil-progress-block">
                            <div className="perfil-progress-label-row">
                              <span>Plano</span>
                              <strong>38%</strong>
                            </div>
                            <div className="perfil-progress-track" aria-hidden="true">
                              <div className="perfil-progress-fill perfil-progress-fill--plano" style={{ width: '38%' }} />
                            </div>
                            <div className="perfil-progress-ends">
                              <span>Escalão 1</span>
                              <span>Escalão 4</span>
                            </div>
                          </div>
                        </div>
                      </div>

                      <div>
                        <p className="theme-debug-kicker">soi-bar-track — objetivos SOI</p>
                        <div className="soi-bars theme-debug-soi-bars" role="list">
                          <AppTooltip label="Mediação · GRO 86">
                            <div className="soi-bar-row" role="listitem">
                              <div className="soi-bar-label">
                                <span>Mediação</span>
                                <strong>86</strong>
                              </div>
                              <div className="soi-bar-track" aria-hidden="true">
                                <div className="soi-bar-fill is-good" style={{ width: '86%' }} />
                              </div>
                            </div>
                          </AppTooltip>
                          <AppTooltip label="Angariação · GRO 54">
                            <div className="soi-bar-row" role="listitem">
                              <div className="soi-bar-label">
                                <span>Angariação</span>
                                <strong>54</strong>
                              </div>
                              <div className="soi-bar-track" aria-hidden="true">
                                <div className="soi-bar-fill is-mid" style={{ width: '54%' }} />
                              </div>
                            </div>
                          </AppTooltip>
                        </div>
                      </div>
                    </div>
                  </ThemeDebugSection>
                  <ThemeDebugSection
                    title="Tabela e paginação"
                    hint="DataGrid — ☰ no cabeçalho abre ordenar/filtrar. Menu Nexora: data-grid-col-menu + AppSelect layout=list compact."
                  >
                    <div className="theme-debug-grid-row">
                      <div className="theme-debug-table">
                        <DataGrid
                          storageKey="theme-debug-grid"
                          showColumnChooser={false}
                          columns={THEME_DEBUG_GRID_COLS}
                          rows={THEME_DEBUG_GRID_ROWS}
                          selectedId={2}
                        />
                        <div className="pagination">
                          <span>1–3 de 3</span>
                          <div className="theme-debug-inline">
                            <button type="button" className="btn btn-secondary btn-sm" disabled>Anterior</button>
                            <button type="button" className="btn btn-secondary btn-sm" disabled>Seguinte</button>
                          </div>
                        </div>
                      </div>
                      <div>
                        <p className="theme-debug-kicker">data-grid-col-menu — ordenar e filtrar</p>
                        <ThemeDebugGridColMenu />
                      </div>
                    </div>
                  </ThemeDebugSection>
                  <ThemeDebugSection title="Toasts, diálogo e erro">
                    <div className="theme-debug-inline">
                      <button type="button" className="btn btn-secondary btn-sm" onClick={() => toast.success('Preferências guardadas.')}>Success</button>
                      <button type="button" className="btn btn-secondary btn-sm" onClick={() => toast.info('Informação de exemplo.')}>Info</button>
                      <button type="button" className="btn btn-secondary btn-sm" onClick={() => toast.warning('Atenção de exemplo.')}>Warning</button>
                      <button type="button" className="btn btn-secondary btn-sm" onClick={() => toast.error('Erro de exemplo.')}>Error</button>
                      <button type="button" className="btn btn-primary btn-sm" onClick={() => setConfirmOpen(true)}>Abrir diálogo</button>
                    </div>
                    <div className="theme-debug-inline-follow">
                      <ErrorBanner error="Exemplo de ErrorBanner — falha ao carregar a lista." />
                    </div>
                  </ThemeDebugSection>
                  <ThemeDebugSection title="Loading">
                    <div className="theme-debug-cards-row">
                      <LoadingSpinner message="A carregar…" />
                      <div className="ds-card theme-debug-demo-card">
                        <CardLoading message="A carregar cartão…" />
                      </div>
                    </div>
                  </ThemeDebugSection>
                </>
              )}

              {tab === 'tokens' && (
                <ThemeDebugSection
                  title="Variáveis CSS aplicadas"
                  hint="Valores computados em :root (app.css + theme.js). Clique para copiar var(--token)."
                >
                  <div className="theme-debug-token-grid theme-debug-token-grid-wide">
                    {visibleVars.map(row => (
                      <ThemeTokenChip key={row.name} name={row.name} value={row.value} />
                    ))}
                  </div>
                  {!visibleVars.length && <p className="form-hint">Nenhum token corresponde ao filtro.</p>}
                </ThemeDebugSection>
              )}
            </div>
          </ModulePageShell>
        </div>
      </div>
      <ConfirmDialog
        open={confirmOpen}
        title="Confirmar"
        message="Confirmar esta acção? Os botões ficam em pílula, no cartão branco."
        onConfirm={() => setConfirmOpen(false)}
        onCancel={() => setConfirmOpen(false)}
      />
    </>
  );
}

function SettingsPage({ onClose, onSaved, initialListKey = null, onColumnsRouteCleared = null, user = null, onOpenTheme = null }) {
  const isAdmin = userIsAdmin(user);
  const [tab, setTab] = useState(initialListKey ? 'sharepoint' : 'perfil');
  const [prefs, setPrefs] = useState(() => loadLocalUserPreferences(user));
  const [prefsSaving, setPrefsSaving] = useState(false);
  const [calSettings, setCalSettings] = useState(() => defaultCalSettings());
  const [canManageSlots, setCanManageSlots] = useState(false);
  const [calSettingsSaving, setCalSettingsSaving] = useState(false);
  const [loading, setLoading] = useState(false);
  const [testResult, setTestResult] = useState(null);
  const [schemaBusy, setSchemaBusy] = useState(false);
  const [lists, setLists] = useState([]);
  const [listLoadState, setListLoadState] = useState({});
  const [gridEditor, setGridEditor] = useState(null);
  const [pendingColumnsListKey, setPendingColumnsListKey] = useState(initialListKey || null);
  const [columnsSaveMessage, setColumnsSaveMessage] = useState('');
  const columnsSaveTimer = useRef(null);
  const formRef = useRef(null);
  const [form, setForm] = useState({
    azureAd: { tenantId: '', clientId: '', clientSecret: '', domain: '', authMode: 'ClientCredentials', callbackPath: '/signin-oidc' },
    microsoftGraph: { baseUrl: 'https://graph.microsoft.com/v1.0', scopes: 'https://graph.microsoft.com/.default' },
    sharePoint: {
      siteUrl: '', negociosListName: 'HABITA - Pipeline', negociosListId: '',
      sites: defaultSharePointSites(),
      columns: {
        operacao: 'Ag_x00ea_ncia', estado: 'Estado', data: 'Data',
        comissaoTotalHabita: 'Comiss_x00e3_oTotalHABITA', probabilidade: 'ProbabilidadedeConcretiza_x00e7_',
        naturezaNegocio: 'Naturezadeneg_x00f3_cio', dataPrevistaFaturacao: 'DataPrevista_x002f_EfetivaConclu',
        dataPrevistaRecebimento: 'Dataprevistaderecebimento', referencia: 'Refer_x00ea_ncia', montante: 'Montante'
      },
      lists: [],
      permissions: {
        operacoesListId: '', permissoesListId: '',
        operacoesListName: 'Operações', permissoesListName: 'HABITA - Permissões'
      }
    },
    hasClientSecret: false
  });
  formRef.current = form;

  useEffect(() => {
    let cancelled = false;
    fetchUserPreferences(user).then(p => {
      if (!cancelled) setPrefs(p);
    });
    return () => { cancelled = true; };
  }, [user?.email, user?.Email]);

  useEffect(() => {
    let cancelled = false;
    apiFetch('/api/calendar').then(doc => {
      if (cancelled) return;
      const mapped = mapCalendarDoc(doc);
      setCanManageSlots(mapped.canManageSlots || userIsAdmin(user));
      setCalSettings(mapped.settings || defaultCalSettings());
    }).catch(() => {
      if (!cancelled) setCalSettings(defaultCalSettings());
    });
    return () => { cancelled = true; };
  }, [user?.email, user?.Email]);

  useEffect(() => {
    setTestResult(null);
    if (!isAdmin) return;
    apiFetch('/api/settings').then(data => {
      setForm({
        azureAd: {
          tenantId: data.azureAd.tenantId || '',
          clientId: data.azureAd.clientId || '',
          clientSecret: '',
          domain: data.azureAd.domain || '',
          authMode: data.azureAd.authMode || 'ClientCredentials',
          callbackPath: data.azureAd.callbackPath || '/signin-oidc'
        },
        microsoftGraph: data.microsoftGraph || { baseUrl: 'https://graph.microsoft.com/v1.0', scopes: 'https://graph.microsoft.com/.default' },
        sharePoint: {
          siteUrl: data.sharePoint.siteUrl || '',
          negociosListName: data.sharePoint.negociosListName || 'HABITA - Pipeline',
          negociosListId: data.sharePoint.negociosListId || '',
          sites: (data.sharePoint.sites && data.sharePoint.sites.length)
            ? data.sharePoint.sites
            : defaultSharePointSites(data.sharePoint.siteUrl || ''),
          columns: data.sharePoint.columns || {},
          lists: (data.sharePoint.lists || []).map(l => ({
            ...l,
            siteKey: l.siteKey || 'habita',
            gridColumns: l.gridColumns || l.GridColumns || [],
            cardLayout: normalizePhoneCardLayout(l.cardLayout || l.CardLayout),
          })),
          permissions: data.sharePoint.permissions || {}
        },
        hasClientSecret: data.azureAd.hasClientSecret
      });
    }).catch(e => {
      if (isAdmin) toast.error('Erro ao carregar definições: ' + e.message);
    });
  }, [isAdmin]);

  const userTabs = [
    { id: 'perfil', label: 'Perfil' },
    { id: 'dashboard', label: 'Dashboard' },
    { id: 'notificacoes', label: 'Notificações' },
    { id: 'fotografia', label: 'Fotografia' },
  ];
  const adminTabs = [
    { id: 'entra', label: 'Entra ID' },
    { id: 'graph', label: 'Microsoft Graph' },
    { id: 'sharepoint', label: 'SharePoint' },
    { id: 'acesso', label: 'Acesso' },
    { id: 'tema', label: 'Tema' },
  ];
  const settingsTabs = isAdmin ? [...userTabs, ...adminTabs] : userTabs;
  const isUserPrefsTab = ['perfil', 'dashboard', 'notificacoes'].includes(tab);
  const isCalSettingsTab = tab === 'fotografia';
  const canEditCalSettings = isAdmin || canManageSlots;

  const patchPrefs = (path, value) => {
    setPrefs(prev => {
      const next = { ...prev };
      if (path.startsWith('notifications.')) {
        const key = path.slice('notifications.'.length);
        next.notifications = { ...next.notifications, [key]: value };
      } else {
        next[path] = value;
      }
      return next;
    });
  };

  const handleSavePrefs = async ({ captureDashboard = false } = {}) => {
    setPrefsSaving(true);
    try {
      let next = { ...prefs };
      if (captureDashboard || tab === 'dashboard') {
        const snap = snapshotCurrentDashboardLayout(user);
        if (snap) next = { ...next, dashboard: snap };
      }
      const saved = await persistUserPreferences(user, next);
      setPrefs(saved);
      toast.success('Preferências guardadas.');
    } catch (err) {
      toast.error(err.message || 'Erro ao guardar preferências.');
    } finally {
      setPrefsSaving(false);
    }
  };

  const handleSaveCalSettings = async () => {
    setCalSettingsSaving(true);
    try {
      const doc = await apiFetch('/api/calendar/settings', {
        method: 'PUT',
        body: JSON.stringify(calSettings),
      });
      const mapped = mapCalendarDoc(doc);
      setCalSettings(mapped.settings || defaultCalSettings());
      setCanManageSlots(mapped.canManageSlots || isAdmin);
      toast.success('Definições de fotografia guardadas.');
    } catch (err) {
      toast.error(err.message || 'Erro ao guardar definições de fotografia.');
    } finally {
      setCalSettingsSaving(false);
    }
  };

  const handleRestoreDashboardDefault = () => {
    if (!confirm('Repor o dashboard ao layout predefinido neste browser?')) return;
    const key = user?.email || user?.Email || 'default';
    try { window.localStorage.removeItem(dashboardLayoutStorageKey(key)); } catch { /* */ }
    setPrefs(prev => ({
      ...prev,
      dashboard: { layoutSavedAt: null, widgets: null },
    }));
    toast.info('Layout local reposto. Recarregue o Dashboard para ver o predefinido.');
  };

  const handleApplySavedDashboard = () => {
    if (applyDashboardLayoutFromPrefs(user, prefs)) {
      toast.success('Layout guardado aplicado. Abra o Dashboard para o ver.');
    } else {
      toast.warning('Não há layout guardado nas preferências.');
    }
  };

  const openColumnsEditor = async (list) => {
    setTab('sharepoint');
    setPendingColumnsListKey(null);
    const path = `/settings/columns/${encodeURIComponent(list.key)}`;
    if (window.location.pathname !== path) {
      window.history.pushState(null, '', path);
    }
    setGridEditor({
      listKey: list.key,
      spColumns: [],
      samplePreview: null,
      sampleRaw: null,
      loading: true,
      error: null,
    });
    try {
      const cols = await apiFetch(`/api/settings/lists/by-key/${encodeURIComponent(list.key)}/columns`);
      let samplePreview = null;
      let sampleRaw = null;
      try {
        const sample = await apiFetch(`/api/lists/${encodeURIComponent(list.key)}/items/debug/fields?count=1`);
        const first = Array.isArray(sample) ? sample[0] : null;
        samplePreview = first?.enrichedFields || first?.fields || first?.Fields || null;
        sampleRaw = first?.rawFields || first?.rawfields || null;
      } catch { /* optional */ }

      const spColumns = Array.isArray(cols) ? cols : [];
      const existing = list.gridColumns || list.GridColumns || [];
      if (existing.length === 0 && spColumns.length > 0) {
        const seeded = buildDefaultGridColumnsFromSp(spColumns);
        setForm(prev => {
          const next = {
            ...prev,
            sharePoint: {
              ...prev.sharePoint,
              lists: (prev.sharePoint.lists || []).map(l =>
                l.key === list.key ? { ...l, gridColumns: seeded } : l
              ),
            },
          };
          scheduleColumnsSave(next);
          return next;
        });
      }

      setGridEditor({
        listKey: list.key,
        spColumns,
        samplePreview,
        sampleRaw,
        loading: false,
        error: null,
      });
    } catch (e) {
      setGridEditor({
        listKey: list.key,
        spColumns: [],
        samplePreview: null,
        sampleRaw: null,
        loading: false,
        error: e.message || 'Erro ao carregar colunas',
      });
    }
  };

  const syncAllGridColumns = async ({ forceUtilizador = true } = {}) => {
    const enabled = (form.sharePoint.lists || []).filter(l => l.enabled);
    if (!enabled.length) {
      toast.warning('Não há listas ativas.');
      return;
    }
    if (!confirm(
      forceUtilizador
        ? `Sincronizar grelhas de ${enabled.length} listas ativas?\nColunas Utilizador ficam Visível (como Pipeline). Listas já mapeadas são atualizadas só se tiverem grelha vazia, excepto se confirmar reposição.`
        : `Sincronizar grelhas vazias de ${enabled.length} listas?`
    )) return;

    setLoading(true);
    try {
      let nextLists = [...(form.sharePoint.lists || [])];
      let updated = 0;
      for (const list of enabled) {
        const existing = list.gridColumns || list.GridColumns || [];
        if (existing.length > 0) continue;
        try {
          const cols = await apiFetch(`/api/settings/lists/by-key/${encodeURIComponent(list.key)}/columns`);
          const spColumns = Array.isArray(cols) ? cols : [];
          if (!spColumns.length) continue;
          const seeded = buildDefaultGridColumnsFromSp(spColumns);
          nextLists = nextLists.map(l => l.key === list.key ? { ...l, gridColumns: seeded } : l);
          updated++;
        } catch (e) {
          console.warn('Falha a sincronizar colunas', list.key, e);
        }
      }
      const nextForm = {
        ...form,
        sharePoint: { ...form.sharePoint, lists: nextLists },
      };
      setForm(nextForm);
      await persistSettings(nextForm, { stay: true });
      if (updated > 0) {
        toast.success(`Grelhas sincronizadas: ${updated} lista(s).`);
      } else {
        toast.info('Todas as listas ativas já tinham mapeamento de colunas.');
      }
    } catch (e) {
      toast.error(e.message || 'Erro ao sincronizar grelhas.');
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    if (!pendingColumnsListKey) return;
    const list = (form.sharePoint.lists || []).find(l => l.key === pendingColumnsListKey);
    if (!list) return;
    openColumnsEditor(list);
  }, [pendingColumnsListKey, form.sharePoint.lists]);

  const closeColumnsEditor = async () => {
    if (columnsSaveTimer.current) {
      clearTimeout(columnsSaveTimer.current);
      columnsSaveTimer.current = null;
      await persistSettings(formRef.current, { stay: true, quiet: true });
    }
    setColumnsSaveMessage('');
    setGridEditor(null);
    setPendingColumnsListKey(null);
    onColumnsRouteCleared?.();
    if (window.location.pathname.startsWith('/settings/columns/')) {
      window.history.pushState(null, '', '/settings');
    }
  };

  const editingList = gridEditor
    ? (form.sharePoint.lists || []).find(l => l.key === gridEditor.listKey)
    : null;

  const buildPayloadFrom = (formState) => {
    const lists = formState.sharePoint.lists || [];
    const pipeline = lists.find(l => l.key === 'pipeline') || lists.find(l => l.enabled && l.module === 'mediacao');
    const sites = formState.sharePoint.sites || defaultSharePointSites(formState.sharePoint.siteUrl);
    const habitaSite = sites.find(s => s.key === 'habita');
    const siteUrl = habitaSite?.siteUrl || formState.sharePoint.siteUrl;

    return {
      azureAd: {
        instance: 'https://login.microsoftonline.com/',
        tenantId: formState.azureAd.tenantId,
        clientId: formState.azureAd.clientId,
        clientSecret: formState.azureAd.clientSecret || undefined,
        domain: formState.azureAd.domain,
        authMode: formState.azureAd.authMode,
        callbackPath: formState.azureAd.callbackPath
      },
      microsoftGraph: formState.microsoftGraph,
      sharePoint: {
        ...formState.sharePoint,
        siteUrl,
        sites,
        negociosListName: pipeline?.displayName || formState.sharePoint.negociosListName,
        negociosListId: pipeline?.listId || formState.sharePoint.negociosListId,
        defaultPageSize: 24,
        maxPageSize: 100,
        columns: {
          operacao: formState.sharePoint.columns.operacao || 'Ag_x00ea_ncia',
          consultor: 'Consultor',
          consultorNome: 'Consultor',
          estado: formState.sharePoint.columns.estado || 'Estado',
          probabilidade: formState.sharePoint.columns.probabilidade || 'ProbabilidadedeConcretiza_x00e7_',
          naturezaNegocio: formState.sharePoint.columns.naturezaNegocio || 'Naturezadeneg_x00f3_cio',
          comissaoTotalHabita: formState.sharePoint.columns.comissaoTotalHabita || 'Comiss_x00e3_oTotalHABITA',
          data: formState.sharePoint.columns.data || 'Data',
          dataPrevistaFaturacao: formState.sharePoint.columns.dataPrevistaFaturacao || 'DataPrevista_x002f_EfetivaConclu',
          dataPrevistaRecebimento: formState.sharePoint.columns.dataPrevistaRecebimento || 'Dataprevistaderecebimento',
          referencia: formState.sharePoint.columns.referencia || 'Refer_x00ea_ncia',
          montante: formState.sharePoint.columns.montante || 'Montante',
          title: 'Title'
        },
        lists,
        permissions: formState.sharePoint.permissions || {}
      }
    };
  };

  const persistSettings = async (formState, { stay = false, quiet = false } = {}) => {
    setLoading(true);
    if (!quiet) setColumnsSaveMessage('A guardar…');
    try {
      await apiFetch('/api/settings', { method: 'PUT', body: JSON.stringify(buildPayloadFrom(formState)) });
      onSaved();
      if (!quiet) setColumnsSaveMessage('Guardado');
      if (!stay) onClose();
    } catch (e) {
      const detail = e?.message || e?.code || 'erro desconhecido';
      if (!quiet) setColumnsSaveMessage(`Falha: ${detail}`);
      toast.error('Erro ao guardar mapeamento de colunas: ' + detail);
      throw e;
    } finally {
      setLoading(false);
    }
  };

  const scheduleColumnsSave = (nextForm) => {
    if (columnsSaveTimer.current) clearTimeout(columnsSaveTimer.current);
    setColumnsSaveMessage('Alterações por guardar…');
    columnsSaveTimer.current = setTimeout(() => {
      columnsSaveTimer.current = null;
      persistSettings(nextForm, { stay: true }).catch(() => {});
    }, 600);
  };

  if (gridEditor && editingList) {
    return (
      <GridColumnsEditorPage
        list={editingList}
        spColumns={gridEditor.spColumns}
        samplePreview={gridEditor.samplePreview}
        sampleRaw={gridEditor.sampleRaw}
        loading={gridEditor.loading}
        error={gridEditor.error}
        onBack={closeColumnsEditor}
        saving={loading}
        saveMessage={columnsSaveMessage}
        onSave={() => {
          if (columnsSaveTimer.current) {
            clearTimeout(columnsSaveTimer.current);
            columnsSaveTimer.current = null;
          }
          persistSettings(formRef.current, { stay: true }).catch(() => {});
        }}
        onChangeGridColumns={(gridColumns) => {
          setForm(prev => {
            const next = {
              ...prev,
              sharePoint: {
                ...prev.sharePoint,
                lists: (prev.sharePoint.lists || []).map(l =>
                  l.key === editingList.key ? { ...l, gridColumns } : l
                ),
              },
            };
            formRef.current = next;
            scheduleColumnsSave(next);
            return next;
          });
        }}
        onChangeCardLayout={(cardLayout) => {
          setForm(prev => {
            const next = {
              ...prev,
              sharePoint: {
                ...prev.sharePoint,
                lists: (prev.sharePoint.lists || []).map(l =>
                  l.key === editingList.key ? { ...l, cardLayout } : l
                ),
              },
            };
            formRef.current = next;
            scheduleColumnsSave(next);
            return next;
          });
        }}
      />
    );
  }

  if (gridEditor && !editingList && gridEditor.loading) {
    return (
      <>
        <ModuleTopbar icon="settings" title="Mapeamento de colunas" actions={(
          <button type="button" className="btn btn-secondary" onClick={closeColumnsEditor}>Voltar</button>
        )} />
        <div className="page-body">
          <LoadingSpinner message="A carregar colunas SharePoint…" />
        </div>
      </>
    );
  }

  const buildPayload = () => buildPayloadFrom(form);

  const handleTest = async () => {
    setLoading(true);
    setTestResult(null);
    try {
      const result = await apiFetch('/api/settings/test', { method: 'POST', body: JSON.stringify(buildPayload()) });
      setTestResult(result);
      if (result.success) {
        try {
          await handleLoadAllLists();
        } catch { /* lists load after save */ }
      }
    } catch (e) {
      setTestResult({ success: false, message: e.message });
    } finally {
      setLoading(false);
    }
  };

  const handleSave = async () => {
    try {
      await persistSettings(form, { stay: false });
    } catch { /* alert already shown */ }
  };

  const updateSiteUrl = (siteKey, siteUrl) => {
    setForm(prev => {
      const sites = (prev.sharePoint.sites || defaultSharePointSites()).map(s =>
        s.key === siteKey ? { ...s, siteUrl } : s);
      return {
        ...prev,
        sharePoint: {
          ...prev.sharePoint,
          sites,
          siteUrl: siteKey === 'habita' ? siteUrl : prev.sharePoint.siteUrl
        }
      };
    });
  };

  const updateListsForSite = (siteKey, updatedSiteLists) => {
    setForm(prev => {
      const others = (prev.sharePoint.lists || []).filter(l => (l.siteKey || 'habita') !== siteKey);
      return {
        ...prev,
        sharePoint: {
          ...prev.sharePoint,
          lists: [...others, ...updatedSiteLists]
        }
      };
    });
  };

  const handleLoadSiteLists = async (siteKey) => {
    const site = (form.sharePoint.sites || []).find(s => s.key === siteKey);
    if (!site?.siteUrl?.trim()) {
      setListLoadState(prev => ({ ...prev, [siteKey]: { loading: false, error: 'Indique o URL do site.' } }));
      return;
    }

    setListLoadState(prev => ({ ...prev, [siteKey]: { loading: true, error: null } }));
    try {
      const params = new URLSearchParams({ siteKey, siteUrl: site.siteUrl.trim() });
      const listData = await apiFetch(`/api/settings/lists?${params}`);
      setLists(prev => {
        const withoutSite = (prev || []).filter(l => l.siteKey !== siteKey);
        return [...withoutSite, ...listData];
      });
      setForm(prev => ({
        ...prev,
        sharePoint: {
          ...prev.sharePoint,
          lists: mergeSiteListsIntoRegistry(listData, prev.sharePoint.lists, siteKey)
        }
      }));
      setListLoadState(prev => ({ ...prev, [siteKey]: { loading: false, error: null } }));
    } catch (e) {
      setListLoadState(prev => ({ ...prev, [siteKey]: { loading: false, error: e.message } }));
    }
  };

  const handleLoadAllLists = async () => {
    const sites = form.sharePoint.sites || defaultSharePointSites();
    for (const site of sites) {
      if (!site.siteUrl?.trim()) continue;
      await handleLoadSiteLists(site.key);
    }
  };

  const applyInvestimentosSchema = async () => {
    setSchemaBusy(true);
    try {
      const result = await apiFetch('/api/settings/investimentos-schema', { method: 'POST' });
      const log = Array.isArray(result.log) ? result.log.join(' · ') : 'Schema aplicado.';
      setTestResult({ success: true, message: log });
      toast.success('Schema Investimentos aplicado nas listas Ativos, Oportunidades e Clientes.');
      onSaved?.();
    } catch (e) {
      setTestResult({ success: false, message: e.message });
      toast.error(e.message || 'Falha ao aplicar schema Investimentos.');
    } finally {
      setSchemaBusy(false);
    }
  };

  return (
    <>
      <ModuleTopbar
        icon="settings"
        title="Definições"
        actions={(
          <button type="button" className="btn btn-secondary" onClick={onClose}>
            Voltar
          </button>
        )}
      />
      <div className="page-body">
        <div className="page-content module-layout settings-page">
          <ModulePageShell
            className="settings-page-shell"
            tabs={(
              <div className="tabs module-tabs settings-page-tabs" role="tablist">
                {settingsTabs.map(t => (
                  <button
                    key={t.id}
                    type="button"
                    className={`tab ${tab === t.id ? 'active' : ''}`}
                    role="tab"
                    aria-selected={tab === t.id}
                    onClick={() => {
                      if (t.id === 'tema') {
                        onOpenTheme?.();
                        return;
                      }
                      setTab(t.id);
                    }}
                  >
                    {t.label}
                  </button>
                ))}
              </div>
            )}
          >
            <div className="settings-page-body">
          {tab === 'perfil' && (
            <div className="settings-prefs">
              <section className="settings-prefs-card">
                <h3>O seu perfil</h3>
                <dl className="settings-prefs-dl">
                  <div><dt>Nome</dt><dd>{userAssinatura(user?.displayName || user?.DisplayName)}</dd></div>
                  <div><dt>Email</dt><dd>{user?.email || user?.Email || '—'}</dd></div>
                  <div><dt>Função</dt><dd>{user?.funcao || user?.Funcao || '—'}</dd></div>
                  <div><dt>Local</dt><dd>{user?.local || user?.Local || '—'}</dd></div>
                  <div>
                    <dt>Perfis</dt>
                    <dd>{formatUserPerfisLabel(user, isAdmin)}</dd>
                  </div>
                </dl>
              </section>
              <section className="settings-prefs-card">
                <h3>Preferências da app</h3>
                <div className="form-group">
                  <label>Página inicial</label>
                  <AppSelect
                    value={prefs.defaultPage}
                    options={[
                      { value: 'dashboard', label: 'Dashboard' },
                      { value: 'mediacao', label: 'Mediação' },
                      { value: 'equipa', label: 'Equipa' },
                      { value: 'carteira', label: 'Investimentos' },
                      { value: 'soi', label: 'SOI' },
                    ]}
                    onChange={v => patchPrefs('defaultPage', v)}
                    ariaLabel="Página inicial"
                  />
                </div>
                <div className="form-group">
                  <label>Ano predefinido no Dashboard</label>
                  <AppSelect
                    value={prefs.dashboardAno || String(new Date().getFullYear())}
                    options={(() => {
                      const y = new Date().getFullYear();
                      return [...new Set([2024, 2025, 2026, 2027, y - 1, y, y + 1])].sort((a, b) => a - b).map(n => ({ value: String(n), label: String(n) }));
                    })()}
                    onChange={v => patchPrefs('dashboardAno', v)}
                    ariaLabel="Ano predefinido no Dashboard"
                  />
                </div>
                <p className="form-hint">Estas preferências ficam associadas à sua conta neste servidor.</p>
              </section>
            </div>
          )}
          {tab === 'dashboard' && (
            <div className="settings-prefs">
              <section className="settings-prefs-card">
                <h3>Layout do Dashboard</h3>
                <p className="form-hint">
                  Guarde o layout atual (widgets e tamanhos) nas suas preferências, para o recuperar noutro browser neste servidor.
                </p>
                <dl className="settings-prefs-dl">
                  <div>
                    <dt>Último layout guardado</dt>
                    <dd>
                      {prefs.dashboard?.layoutSavedAt
                        ? formatDateTimePt(prefs.dashboard.layoutSavedAt)
                        : 'Ainda não guardado'}
                    </dd>
                  </div>
                  <div>
                    <dt>Widgets no layout guardado</dt>
                    <dd>{prefs.dashboard?.widgets?.length || 0}</dd>
                  </div>
                </dl>
                <div className="settings-prefs-actions">
                  <button
                    type="button"
                    className="btn btn-primary"
                    disabled={prefsSaving}
                    onClick={() => handleSavePrefs({ captureDashboard: true })}
                  >
                    {prefsSaving ? 'A guardar…' : 'Guardar layout atual'}
                  </button>
                  <button type="button" className="btn btn-secondary" onClick={handleApplySavedDashboard}>
                    Aplicar layout guardado
                  </button>
                  <button type="button" className="btn btn-secondary" onClick={handleRestoreDashboardDefault}>
                    Repor layout predefinido
                  </button>
                </div>
              </section>
            </div>
          )}
          {tab === 'notificacoes' && (
            <div className="settings-prefs">
              <section className="settings-prefs-card">
                <h3>Canais</h3>
                <label className="settings-check">
                  <input
                    type="checkbox"
                    checked={!!prefs.notifications.inAppEnabled}
                    onChange={e => patchPrefs('notifications.inAppEnabled', e.target.checked)}
                  />
                  Receber notificações na aplicação (sino)
                </label>
                <label className="settings-check">
                  <input
                    type="checkbox"
                    checked={!!prefs.notifications.emailEnabled}
                    onChange={e => patchPrefs('notifications.emailEnabled', e.target.checked)}
                  />
                  Receber notificações por email
                </label>
              </section>
              <section className="settings-prefs-card">
                <h3>Tipos de notificação</h3>
                <label className="settings-check">
                  <input
                    type="checkbox"
                    checked={!!prefs.notifications.avaliacaoAuto}
                    onChange={e => patchPrefs('notifications.avaliacaoAuto', e.target.checked)}
                    disabled={!prefs.notifications.inAppEnabled && !prefs.notifications.emailEnabled}
                  />
                  Avaliação — lembrete de autoavaliação
                </label>
                <label className="settings-check">
                  <input
                    type="checkbox"
                    checked={!!prefs.notifications.avaliacaoGestao}
                    onChange={e => patchPrefs('notifications.avaliacaoGestao', e.target.checked)}
                    disabled={!prefs.notifications.inAppEnabled}
                  />
                  Avaliação — gestão de equipa (diretores / gestores)
                </label>
                <label className="settings-check">
                  <input
                    type="checkbox"
                    checked={!!prefs.notifications.lembretesEmail}
                    onChange={e => patchPrefs('notifications.lembretesEmail', e.target.checked)}
                    disabled={!prefs.notifications.emailEnabled}
                  />
                  Emails de lembrete enviados pela administração
                </label>
                <p className="form-hint">Os lembretes em massa respeitam estas opções por colaborador.</p>
              </section>
            </div>
          )}
          {tab === 'fotografia' && (
            <div className="settings-prefs">
              <section className="settings-prefs-card">
                <h3>Visita de fotografia</h3>
                <div className="form-group">
                  <label>Antecedência mínima (horas)</label>
                  <input
                    type="number"
                    min={0}
                    max={72}
                    step={1}
                    value={calSettings.photoMinHoursAhead}
                    disabled={!canEditCalSettings}
                    onChange={e => {
                      const n = Number(e.target.value);
                      setCalSettings(s => ({
                        ...s,
                        photoMinHoursAhead: Number.isFinite(n) ? Math.min(72, Math.max(0, n)) : 0,
                      }));
                    }}
                  />
                  <p className="form-hint">
                    O pedido só é aceite se o início da visita for pelo menos este número de horas depois de agora.
                    {' '}Nunca no passado. Predefinição: 2 horas.
                  </p>
                </div>
              </section>
              {canEditCalSettings && (
                <section className="settings-prefs-card">
                  <h3>Horário de trabalho do fotógrafo</h3>
                  <p className="form-hint">
                    Também pode alterar este horário no calendário, no botão Horário de trabalho.
                  </p>
                  <div className="chart-filter-row">
                    <div className="form-group">
                      <label>Início</label>
                      <TimePickerField
                        value={calSettings.workStart}
                        onChange={workStart => setCalSettings(s => ({ ...s, workStart }))}
                      />
                    </div>
                    <div className="form-group">
                      <label>Fim</label>
                      <TimePickerField
                        value={calSettings.workEnd}
                        min={calSettings.workStart || undefined}
                        onChange={workEnd => setCalSettings(s => ({ ...s, workEnd }))}
                      />
                    </div>
                  </div>
                  <label className="settings-check">
                    <input
                      type="checkbox"
                      checked={calSettings.lunchEnabled !== false}
                      onChange={e => setCalSettings(s => ({ ...s, lunchEnabled: e.target.checked }))}
                    />
                    Pausa de almoço
                  </label>
                  {calSettings.lunchEnabled !== false && (
                    <div className="chart-filter-row">
                      <div className="form-group">
                        <label>Almoço início</label>
                        <TimePickerField
                          value={calSettings.lunchStart}
                          min={calSettings.workStart || undefined}
                          max={calSettings.workEnd || undefined}
                          onChange={lunchStart => setCalSettings(s => ({ ...s, lunchStart }))}
                        />
                      </div>
                      <div className="form-group">
                        <label>Almoço fim</label>
                        <TimePickerField
                          value={calSettings.lunchEnd}
                          min={calSettings.lunchStart || calSettings.workStart || undefined}
                          max={calSettings.workEnd || undefined}
                          onChange={lunchEnd => setCalSettings(s => ({ ...s, lunchEnd }))}
                        />
                      </div>
                    </div>
                  )}
                  <p className="form-hint">
                    Por predefinição não se aceitam visitas de fotografia nesta pausa. Altere o intervalo ou desmarque se precisar de agendar nessa hora.
                  </p>
                  <label className="settings-check">
                    <input
                      type="checkbox"
                      checked={!!calSettings.weekendsEnabled}
                      onChange={e => setCalSettings(s => ({ ...s, weekendsEnabled: e.target.checked }))}
                    />
                    Disponível ao fim de semana
                  </label>
                </section>
              )}
              {!canEditCalSettings && (
                <p className="form-hint">Só o fotógrafo ou um administrador pode alterar estas definições.</p>
              )}
            </div>
          )}
          {tab === 'entra' && (
            <>
              <div className="form-group">
                <label>Tenant ID</label>
                <input value={form.azureAd.tenantId} onChange={e => setForm({ ...form, azureAd: { ...form.azureAd, tenantId: e.target.value } })} placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" />
              </div>
              <div className="form-group">
                <label>Client ID (App Registration)</label>
                <input value={form.azureAd.clientId} onChange={e => setForm({ ...form, azureAd: { ...form.azureAd, clientId: e.target.value } })} />
              </div>
              <div className="form-group">
                <label>Client Secret {form.hasClientSecret && '(deixe vazio para manter)'}</label>
                <input type="password" value={form.azureAd.clientSecret} onChange={e => setForm({ ...form, azureAd: { ...form.azureAd, clientSecret: e.target.value } })} placeholder="••••••••" />
                <p className="form-hint">Cole o <strong>Value</strong> do secret (ex.: <code>abc~XYZ...</code>), não o Secret ID (GUID).</p>
              </div>
              <div className="form-row-2">
                <div className="form-group">
                  <label>Domain</label>
                  <input value={form.azureAd.domain} onChange={e => setForm({ ...form, azureAd: { ...form.azureAd, domain: e.target.value } })} placeholder="empresa.onmicrosoft.com" />
                </div>
                <div className="form-group">
                  <label>Auth Mode</label>
                  <AppSelect
                    value={form.azureAd.authMode}
                    options={[
                      { value: 'ClientCredentials', label: 'Client Credentials (app-only)' },
                      { value: 'Delegated', label: 'Delegated (user login)' },
                    ]}
                    onChange={v => setForm({ ...form, azureAd: { ...form.azureAd, authMode: v } })}
                    ariaLabel="Auth Mode"
                  />
                </div>
              </div>
              <p className="form-hint">Registe a app no Entra ID com permissão <strong>Sites.Selected</strong> e conceda acesso ao site SharePoint via PowerShell.</p>
            </>
          )}
          {tab === 'graph' && (
            <>
              <div className="form-group">
                <label>Graph Base URL</label>
                <input value={form.microsoftGraph.baseUrl} onChange={e => setForm({ ...form, microsoftGraph: { ...form.microsoftGraph, baseUrl: e.target.value } })} />
              </div>
              <div className="form-group">
                <label>Scopes</label>
                <input value={form.microsoftGraph.scopes} onChange={e => setForm({ ...form, microsoftGraph: { ...form.microsoftGraph, scopes: e.target.value } })} />
                <p className="form-hint">Client Credentials usa <code>https://graph.microsoft.com/.default</code></p>
              </div>
            </>
          )}
          {tab === 'sharepoint' && (
            <>
              {isAdmin && <CacheStatusPanel />}
              <p className="form-hint" style={{ marginBottom: '1rem' }}>
                Configure cada site SharePoint separadamente. Carregue as listas, marque as que a app pode usar e clique em <strong>Colunas</strong> em cada lista para mapear as colunas da grelha (campo SharePoint, rótulo e propriedade a mostrar).
              </p>
              {(form.sharePoint.sites || defaultSharePointSites()).map(site => {
                const siteLists = (form.sharePoint.lists || []).filter(l => (l.siteKey || 'habita') === site.key);
                const loadState = listLoadState[site.key] || {};
                return (
                  <div key={site.key} className="site-connection-block">
                    <div className="site-connection-title-row">
                      <BrandLogo variant={site.key === 'himo' ? 'himo' : 'habita'} className="site-connection-logo" />
                      <h3 className="site-connection-title">{site.name}</h3>
                    </div>
                    <div className="form-group">
                      <label>Site URL — {site.name}</label>
                      <input
                        value={site.siteUrl || ''}
                        onChange={e => updateSiteUrl(site.key, e.target.value)}
                        placeholder={site.key === 'habita'
                          ? 'https://tenant.sharepoint.com/teams/HABITA'
                          : 'https://tenant.sharepoint.com/teams/01HIMOV360'}
                      />
                    </div>
                    <SiteListPicker
                      site={site}
                      lists={siteLists}
                      loading={!!loadState.loading}
                      error={loadState.error}
                      onLoad={() => handleLoadSiteLists(site.key)}
                      onChange={updated => updateListsForSite(site.key, updated)}
                      onOpenColumns={openColumnsEditor}
                    />
                  </div>
                );
              })}
              <button type="button" className="btn btn-secondary btn-sm" onClick={handleLoadAllLists} style={{ marginBottom: '1rem', marginRight: '0.5rem' }}>
                Carregar listas de todos os sites
              </button>
              <button
                type="button"
                className="btn btn-primary btn-sm"
                onClick={() => syncAllGridColumns()}
                disabled={loading}
                style={{ marginBottom: '1rem' }}
                title="Para listas ativas sem mapeamento: marca Visível em colunas Utilizador e aplica formatos (moeda/data)"
              >
                {loading ? 'A sincronizar…' : 'Sincronizar grelhas (Utilizador)'}
              </button>
              {isAdmin && (
                <button
                  type="button"
                  className="btn btn-secondary btn-sm"
                  onClick={applyInvestimentosSchema}
                  disabled={schemaBusy}
                  style={{ marginBottom: '1rem', marginLeft: '0.5rem' }}
                >
                  {schemaBusy ? 'A aplicar schema…' : 'Aplicar schema Investimentos'}
                </button>
              )}
            </>
          )}
          {tab === 'acesso' && (
            <>
              <p className="form-hint" style={{ marginBottom: '0.75rem' }}>
                Gestão da lista <strong>HABITA - Permissões</strong>. Códigos:
                {' '}<code>C</code> = botão Novo, <code>R</code> = ver dados, <code>U</code> = Editar, <code>D</code> = Eliminar.
                {' '}<strong><code>A</code> = Administrador</strong> (CRUD completo + todas as operações).
                {' '}Função = perfil do colaborador na app (lista HIMO Funções).
                {' '}Operações selecionadas = filtro de localização dos negócios (vazio = todas).
              </p>
              <GenericListPage
                user={user}
                listKey="permissoes"
                columns={ACESSO_PERMISSOES_COLUMNS}
                hideHeader
                hideTitleField
                embedded
                gridStorageKey="settings-acesso-permissoes"
                onConfigureListColumns={(key) => {
                  const list = (form.sharePoint.lists || []).find(l => l.key === key);
                  if (list) openColumnsEditor(list);
                }}
              />
            </>
          )}
          {testResult && (
            <div className={`test-result ${testResult.success ? 'ok' : 'fail'}`}>
              {testResult.success ? '✓ ' : '✗ '}{testResult.message}
            </div>
          )}
            </div>
            <div className="settings-page-footer">
              <button type="button" className="btn btn-secondary" onClick={onClose}>
                {isUserPrefsTab || isCalSettingsTab ? 'Voltar' : 'Cancelar'}
              </button>
              {isCalSettingsTab ? (
                canEditCalSettings ? (
                  <button
                    type="button"
                    className="btn btn-primary"
                    onClick={handleSaveCalSettings}
                    disabled={calSettingsSaving}
                  >
                    {calSettingsSaving ? 'A guardar…' : 'Guardar fotografia'}
                  </button>
                ) : null
              ) : isUserPrefsTab ? (
                <button
                  type="button"
                  className="btn btn-primary"
                  onClick={() => handleSavePrefs({ captureDashboard: tab === 'dashboard' })}
                  disabled={prefsSaving}
                >
                  {prefsSaving ? 'A guardar…' : 'Guardar preferências'}
                </button>
              ) : (
                <>
                  <button type="button" className="btn btn-secondary" onClick={handleTest} disabled={loading}>
                    {loading ? 'A testar...' : 'Testar ligação'}
                  </button>
                  <button type="button" className="btn btn-primary" onClick={handleSave} disabled={loading}>
                    Guardar &amp; Aplicar
                  </button>
                </>
              )}
            </div>
          </ModulePageShell>
        </div>
      </div>
    </>
  );
}

function App() {
  const [user, setUser] = useState(null);
  const [realUser, setRealUser] = useState(null);
  const [viewAsUser, setViewAsUser] = useState(null);
  const [authConfig, setAuthConfig] = useState({ requiresLogin: false, loginUrl: '/login' });
  const [config, setConfig] = useState({ isLiveConnected: false });
  const [enabledModules, setEnabledModules] = useState(['mediacao']);
  const [enabledListKeys, setEnabledListKeys] = useState([]);
  const [loading, setLoading] = useState(true);
  const [authReady, setAuthReady] = useState(false);
  const [route, setRoute] = useState(parseAppRoute);
  const [settingsReturnRoute, setSettingsReturnRoute] = useState(null);
  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
  const [dataKey, setDataKey] = useState(0);
  const viewportMode = useViewportMode();
  const isPhone = viewportMode === 'phone';
  const isTablet = viewportMode === 'tablet';
  const [navDrawerOpen, setNavDrawerOpen] = useState(false);
  const [tabletNavExpanded, setTabletNavExpanded] = useState(false);
  const [showReleaseNotes, setShowReleaseNotes] = useState(false);

  // Phone: drawer with full labels. Tablet: icon rail by default, expandable. PC: user preference.
  const effectiveSidebarCollapsed = isPhone
    ? false
    : (isTablet ? !tabletNavExpanded : sidebarCollapsed);

  useEffect(() => {
    if (!isPhone) setNavDrawerOpen(false);
  }, [isPhone]);

  useEffect(() => {
    if (!isTablet) setTabletNavExpanded(false);
  }, [isTablet]);

  const openNav = useCallback(() => setNavDrawerOpen(true), []);
  const closeNav = useCallback(() => setNavDrawerOpen(false), []);

  useEffect(() => {
    const onOpen = () => setShowReleaseNotes(true);
    window.addEventListener('myhabita:open-release-notes', onOpen);
    return () => window.removeEventListener('myhabita:open-release-notes', onOpen);
  }, []);

  const closeReleaseNotes = useCallback(() => {
    markReleaseSeen(APP_RELEASE.id);
    setShowReleaseNotes(false);
  }, []);

  const viewportNavValue = useMemo(() => ({
    mode: viewportMode,
    isPhone,
    isTablet,
    isPc: viewportMode === 'pc',
    navOpen: navDrawerOpen,
    openNav,
    closeNav,
  }), [viewportMode, isPhone, isTablet, navDrawerOpen, openNav, closeNav]);

  const page = route.page;
  const routeTab = route.tab;
  const settingsListKey = route.listKey || null;

  const navigate = useCallback((nextPage, nextTab, nextListKey, filters) => {
    const path = filters
      ? buildPageUrl(nextPage, nextTab, filters, nextListKey)
      : buildAppRoute(nextPage, nextTab, nextListKey);
    const search = path.includes('?') ? path.slice(path.indexOf('?')) : '';
    setRoute({
      page: nextPage,
      tab: nextTab ?? null,
      listKey: nextListKey ?? null,
      search,
    });
    const current = `${window.location.pathname}${window.location.search}`;
    if (current !== path) {
      window.history.pushState(null, '', path);
    }
  }, []);

  const openSettings = useCallback(() => {
    if (route.page !== 'settings') {
      setSettingsReturnRoute({ page: route.page, tab: route.tab, listKey: null });
    }
    navigate('settings');
  }, [navigate, route]);

  const openListColumnsSettings = useCallback((listKey) => {
    if (route.page !== 'settings') {
      setSettingsReturnRoute({ page: route.page, tab: route.tab, listKey: null });
    }
    navigate('settings', 'columns', listKey);
  }, [navigate, route]);

  const closeSettings = useCallback(() => {
    const target = settingsReturnRoute || { page: 'dashboard', tab: null, listKey: null };
    setSettingsReturnRoute(null);
    navigate(target.page, target.tab, target.listKey);
  }, [navigate, settingsReturnRoute]);

  const refreshConfig = async () => {
    let authCfg = { requiresLogin: false, loginUrl: '/login' };
    try {
      authCfg = await fetch('/api/auth/config', { credentials: 'include' }).then(r => r.json());
      setAuthConfig(authCfg);
    } catch {
      setAuthConfig({ requiresLogin: false, loginUrl: '/login' });
    }

    let nextUser = null;
    try {
      const meRes = await Promise.race([
        fetch('/api/auth/me', {
          credentials: 'include',
          headers: { Accept: 'application/json' },
        }),
        new Promise((_, reject) => setTimeout(() => reject(new Error('auth/me timeout')), 8000)),
      ]);
      if (meRes && meRes.ok) nextUser = await meRes.json();
    } catch {
      // Retry once before treating as logged out (slow SharePoint / cold start).
      try {
        const retry = await Promise.race([
          fetch('/api/auth/me', {
            credentials: 'include',
            headers: { Accept: 'application/json' },
          }),
          new Promise((_, reject) => setTimeout(() => reject(new Error('auth/me timeout')), 8000)),
        ]);
        if (retry && retry.ok) nextUser = await retry.json();
      } catch {
        nextUser = null;
      }
    }
    setUser(nextUser);
    setAuthReady(true);
    // Unblock page chrome once identity is known; modules can still load in background.
    setLoading(false);
    if (nextUser) startAuthKeepAlive();

    // Background profile enrichment (admin / função / commission). Fast /me may omit these
    // when SharePoint is cold — without this, admin UI (e.g. dashboard eye) needs a refresh.
    // Use full=true (not full=1): ASP.NET bool binder rejects "1".
    if (nextUser) {
      fetch('/api/auth/me?full=true', {
        credentials: 'include',
        headers: { Accept: 'application/json' },
      })
        .then(r => (r.ok ? r.json() : null))
        .then(enriched => {
          if (!enriched || typeof enriched !== 'object') return;
          // Never apply ASP.NET validation-error payloads or empty objects.
          if (enriched.title && enriched.status && enriched.errors) return;
          if (enriched.isAdmin == null && enriched.IsAdmin == null && !enriched.email && !enriched.Email) return;
          setUser(prev => (prev ? { ...prev, ...enriched } : enriched));
        })
        .catch(() => { /* keep bootstrap profile */ });
    }

    // Not signed in — stop bootstrap here so LoginPage can show
    if (authCfg.requiresLogin && !nextUser) {
      return;
    }

    try {
      const boot = Promise.all([
        fetch('/api/sharepoint/config', { credentials: 'include' }).then(r => r.json()),
        fetch('/api/settings/enabled-lists', { credentials: 'include' }).then(r => r.ok ? r.json() : []),
      ]);
      const [spCfg, lists] = await Promise.race([
        boot,
        new Promise((_, reject) => setTimeout(() => reject(new Error('bootstrap timeout')), 8000)),
      ]);
      setConfig(spCfg);
      const modules = [...new Set((lists || []).map(l => l.module).filter(Boolean))];
      if (modules.length === 0) modules.push('mediacao');
      setEnabledModules(modules);
      setEnabledListKeys((lists || []).map(l => l.key).filter(Boolean));
    } catch { /* optional bootstrap data */ }
  };

  useEffect(() => {
    let settled = false;
    const hardStop = setTimeout(() => {
      if (!settled) {
        setAuthReady(true);
        setLoading(false);
      }
    }, 5000);
    refreshConfig().finally(() => {
      settled = true;
      clearTimeout(hardStop);
      setAuthReady(true);
      setLoading(false);
    });
    return () => clearTimeout(hardStop);
  }, []);

  useEffect(() => {
    if (window.location.hash.startsWith('#/')) {
      const legacyPath = `/${window.location.hash.replace(/^#\/?/, '').trim()}`;
      window.history.replaceState(null, '', legacyPath);
    } else if (window.location.pathname === '/' || window.location.pathname === '/index.html') {
      window.history.replaceState(null, '', '/dashboard');
    }

    const syncRoute = () => {
      setRoute({ ...parseAppRoute(), search: window.location.search || '' });
    };

    syncRoute();
    window.addEventListener('popstate', syncRoute);
    return () => window.removeEventListener('popstate', syncRoute);
  }, []);

  useEffect(() => {
    if (loading) return;
    if (page === 'dashboard' && !isNavItemEnabled({ id: 'dashboard' }, enabledModules)) {
      navigate(enabledModules.includes('mediacao') ? 'mediacao' : 'equipa');
    }
  }, [loading, page, enabledModules, navigate]);

  const handleSettingsSaved = () => {
    refreshConfig();
    setDataKey(k => k + 1);
  };

  const displayUser = viewAsUser || user;
  const canViewAs = userCanViewAs(realUser || user) && !viewAsUser;

  const handleEnterAs = async (member) => {
    const email = memberEmail(member) || member?.email || member?.Email;
    if (!email) throw new Error('Colaborador sem email.');
    const profile = await apiFetch(`/api/auth/view-as?email=${encodeURIComponent(email)}`);
    if (!realUser && user) setRealUser(user);
    window.__myHabitaViewAsEmail = email;
    setViewAsUser({
      ...profile,
      email: profile.email || profile.Email || email,
      displayName: memberAssinatura(member) || profile.displayName,
      colaboradorId: profile.colaboradorId || profile.ColaboradorId || member?.id || member?.Id || null,
      // Prefer colaborador Função/Local when view-as payload is still raw/unenriched.
      funcao: profile.funcao || profile.Funcao || memberField(member, 'funcao', 'Funcao') || null,
      local: profile.local || profile.Local || memberField(member, 'local', 'Local') || null,
      // Prefer scope flags from API so UI operação filters match backend SeeAll.
      allowedOperacoes: profile.negocioSeeAll
        ? []
        : (profile.negocioAllowedOperacoes || profile.allowedOperacoes || []),
    });
    setDataKey(k => k + 1);
    navigate('dashboard');
  };

  const handleExitViewAs = () => {
    window.__myHabitaViewAsEmail = null;
    setViewAsUser(null);
    setRealUser(null);
    setDataKey(k => k + 1);
  };

  const handleLogout = () => {
    window.location.href = '/logout';
  };

  // Stay on login until identity is known — never flash the app shell then bounce back.
  if (!authReady || (authConfig.requiresLogin && !user)) {
    return <LoginPage loginUrl={authConfig.loginUrl} checking={!authReady} />;
  }

  const displayUserSafe = displayUser || { displayName: '…', roles: [] };
  const modulesReady = enabledModules;

  return (
    <ViewportNavContext.Provider value={viewportNavValue}>
      <div
        className={[
          'app-shell',
          effectiveSidebarCollapsed ? 'sidebar-collapsed' : '',
          isPhone && navDrawerOpen ? 'nav-open' : '',
        ].filter(Boolean).join(' ')}
        data-viewport={viewportMode}
      >
        <div className="app-panel">
          {isPhone && navDrawerOpen && (
            <div className="nav-backdrop" onClick={closeNav} aria-hidden="true" />
          )}
          <div className="sidebar-wrap">
            <Sidebar
              user={displayUserSafe}
              activePage={page}
              onNavigate={(nextPage) => {
                if (loading) return;
                closeNav();
                navigate(nextPage);
              }}
              isLiveConnected={config.isLiveConnected}
              showThemeDebug={userIsAdmin(realUser || user)}
              onOpenTheme={() => {
                closeNav();
                navigate('theme');
              }}
              onOpenSettings={() => {
                closeNav();
                openSettings();
              }}
              enabledModules={modulesReady}
              onLogout={handleLogout}
              requiresLogin={authConfig.requiresLogin}
              collapsed={effectiveSidebarCollapsed}
              isViewingAs={Boolean(viewAsUser)}
              onExitViewAs={handleExitViewAs}
              onOpenProfile={(u) => {
                const em = u?.email || u?.Email;
                closeNav();
                navigate('perfil', profileSlugFromEmail(em) || null);
              }}
            />
            {!isPhone && (
              <button
                type="button"
                className="sidebar-collapse-btn"
                onClick={() => {
                  if (isTablet) setTabletNavExpanded(v => !v);
                  else setSidebarCollapsed(c => !c);
                }}
                title={effectiveSidebarCollapsed ? 'Expandir menu' : 'Recolher menu'}
                aria-label={effectiveSidebarCollapsed ? 'Expandir menu' : 'Recolher menu'}
              >
                <i
                  className={`fa-solid ${effectiveSidebarCollapsed ? 'fa-chevron-right' : 'fa-chevron-left'}`}
                  aria-hidden="true"
                />
              </button>
            )}
          </div>
          <main className="main">
            {loading ? (
              <div className="page-body">
                <div className="page-content dashboard-v2-layout">
                  <div className="kpi-row-v2">
                    {KPI_PLACEHOLDERS.map((kpi, i) => (
                      <DashboardKpiCard key={kpi.label} kpi={kpi} icon={['home', 'graph', 'thumb', 'document'][i]} loading />
                    ))}
                  </div>
                  <div className="dashboard-main-grid">
                    <div className="ds-card">
                      <div className="ds-card-head"><h3>Pipeline</h3></div>
                      <CardLoading message="A carregar..." />
                    </div>
                    <div className="dashboard-side-col">
                      <div className="ds-card"><div className="ds-card-head"><h3>O seu Progresso</h3></div><CardLoading /></div>
                      <div className="ds-card"><div className="ds-card-head"><h3>Desafios do Mês</h3></div><CardLoading /></div>
                    </div>
                  </div>
                </div>
              </div>
            ) : (
              <>
            {page === 'calendario' && (
              <CalendarPage user={displayUser} onNavigate={navigate} />
            )}
            {page === 'dashboard' && isNavItemEnabled({ id: 'dashboard' }, enabledModules) && (
              <DashboardPage
                user={displayUser}
                actorUser={realUser || user}
                enabledModules={enabledModules}
                isViewingAs={Boolean(viewAsUser)}
                onNavigate={navigate}
              />
            )}
            {page === 'equipa' && enabledModules.includes('equipa') && (
              <EquipaPage
                user={displayUser}
                equipaEnabled={enabledListKeys.includes('himo-colaboradores')}
                canViewAs={canViewAs}
                onEnterAs={handleEnterAs}
                activeTab={routeTab}
                onTabChange={(tab) => navigate('equipa', tab)}
                onConfigureListColumns={openListColumnsSettings}
              />
            )}
            {page === 'equipa' && !enabledModules.includes('equipa') && (
              <ModuleDisabledMessage moduleName="Equipa" />
            )}
            {page === 'mediacao' && enabledModules.includes('mediacao') && (
              <MediacaoPage
                key={`${dataKey}${route.search || ''}`}
                user={displayUser}
                activeTab={routeTab}
                onTabChange={(tab) => navigate('mediacao', tab)}
                onConfigureListColumns={openListColumnsSettings}
              />
            )}
            {page === 'mediacao' && !enabledModules.includes('mediacao') && (
              <ModuleDisabledMessage moduleName="Mediação" />
            )}
            {page === 'intermediacao' && enabledModules.includes('intermediacao') && (
              <IntermediacaoPage key={dataKey} user={displayUser} onConfigureListColumns={openListColumnsSettings} />
            )}
            {page === 'intermediacao' && !enabledModules.includes('intermediacao') && (
              <ModuleDisabledMessage moduleName="Intermediação" />
            )}
            {page === 'angariacao' && enabledModules.includes('angariacao') && (
              <AngariacaoPage key={dataKey} user={displayUser} onConfigureListColumns={openListColumnsSettings} />
            )}
            {page === 'angariacao' && !enabledModules.includes('angariacao') && (
              <ModuleDisabledMessage moduleName="Angariação" />
            )}
            {page === 'carteira' && enabledModules.includes('carteira') && (
              <CarteiraPage
                key={dataKey}
                user={displayUser}
                activeTab={routeTab}
                onTabChange={(tab) => navigate('carteira', tab)}
                onConfigureListColumns={openListColumnsSettings}
              />
            )}
            {page === 'carteira' && !enabledModules.includes('carteira') && (
              <ModuleDisabledMessage moduleName="Investimentos" />
            )}
            {page === 'soi' && enabledModules.includes('soi') && (
              <SoiPage key={dataKey} user={displayUser} onConfigureListColumns={openListColumnsSettings} />
            )}
            {page === 'soi' && !enabledModules.includes('soi') && (
              <ModuleDisabledMessage moduleName="SOI" />
            )}
            {page === 'perfil' && (
              <ConsultorProfilePage
                key={`perfil-${routeTab || displayUser?.email || displayUser?.Email || 'me'}`}
                user={displayUser}
                profileEmail={routeTab}
                onBack={() => navigate('dashboard')}
              />
            )}
            {page === 'theme' && (
              <ThemeDebugPage
                user={realUser || user}
                onBack={() => navigate('dashboard')}
              />
            )}
            {page === 'settings' && (
              <SettingsPage
                onClose={closeSettings}
                onSaved={handleSettingsSaved}
                initialListKey={settingsListKey}
                onColumnsRouteCleared={() => {
                  setRoute(prev => ({ ...prev, page: 'settings', tab: null, listKey: null }));
                }}
                user={displayUser}
                onOpenTheme={() => navigate('theme')}
              />
            )}
              </>
            )}
          </main>
        </div>
      </div>
      {showReleaseNotes && (
        <ReleaseNotesModal release={APP_RELEASE} onClose={closeReleaseNotes} />
      )}
    </ViewportNavContext.Provider>
  );
}

window.MyHabitaUi = {
  apiFetch,
  toast,
  Icon,
  ConfirmDialog,
  LoadingSpinner,
  ErrorBanner,
  DataGrid,
  userIsAdmin,
  fetchListGridColumns,
  MapearColunasToolbarButton,
  getFieldValue,
  getItemFields,
  formatCellValue,
  EmailInputField,
  ContactoInputField,
  emailFieldError,
  contactoFieldError,
  isValidEmailAddress,
  digitsOnly,
  useOverlayDismiss,
  NumericInputField,
  numericFormKind,
  DatePickerField,
  TimePickerField,
  AppSelect,
  formatDatePt,
  formatTimePt,
};

const root = ReactDOM.createRoot(document.getElementById('root'));
class AppErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null };
  }

  componentDidCatch(error, info) {
    // eslint-disable-next-line no-console
    console.error('App crashed:', error, info);
    this.setState({ error });
  }

  render() {
    if (!this.state.error) return this.props.children;
    const msg = this.state.error?.message || String(this.state.error);
    return (
      <div style={{
        padding: 24,
        fontFamily: "var(--font-body, 'Roboto', system-ui, sans-serif)",
        background: '#fff',
        color: '#111',
        borderRadius: 12,
        margin: 24,
        boxShadow: '0 10px 30px rgba(0,0,0,0.15)',
      }}>
        <h2 style={{ marginBottom: 8 }}>Erro ao carregar a aplicação</h2>
        <p style={{ marginBottom: 0, whiteSpace: 'pre-wrap' }}>{msg}</p>
      </div>
    );
  }
}

root.render(
  <AppErrorBoundary>
    <>
      <ToastHost />
      <App />
    </>
  </AppErrorBoundary>
);
