/* ============================================================
   AGENDA — App (shell + roteamento + estado vindo da API)
   ============================================================ */
function InternalChatScreen() {
  return (
    <div className="content-inner fadein">
      <window.Header title="Chat interno" sub="Comunicação da equipe" />
      <div className="card card-pad">
        <window.Empty icon="mail" title="Chat interno em desenvolvimento" desc="Este recurso é exclusivo do plano Empresarial e estará disponível em breve." />
      </div>
    </div>
  );
}

function MissingScreen({ title = 'Tela nao encontrada' }) {
  return (
    <div className="content-inner fadein">
      <div className="card card-pad">
        <window.Empty icon="info" title={title} desc="Esta tela nao foi carregada corretamente. Atualize o sistema e tente novamente." />
      </div>
    </div>
  );
}

function App() {
  const DB = window.DB;
  const API = window.API;
  const routes = ['dashboard', 'calendario', 'clientes', 'fornecedores', 'servicos', 'equipe', 'financeiro', 'chat', 'tarefas', 'historico', 'usuarios', 'mensalidade', 'config'];
  const [theme, setTheme] = React.useState(() => localStorage.getItem('ag-theme') || 'light');
  const [accent, setAccent] = React.useState(() => localStorage.getItem('ag-accent') || 'indigo');
  const [businessType, setBusinessTypeState] = React.useState(() => localStorage.getItem('ag-biz') || 'geral');
  const [route, setRoute] = React.useState(() => {
    const saved = localStorage.getItem('ag-route');
    return routes.includes(saved) ? saved : 'dashboard';
  });
  const [collapsed, setCollapsed] = React.useState(() => localStorage.getItem('ag-collapsed') === '1');
  const [navOpen, setNavOpen] = React.useState(false); // gaveta da sidebar no mobile

  // botão do menu: no mobile abre/fecha a gaveta; no desktop recolhe a sidebar
  const toggleMenu = () => {
    if (window.innerWidth <= 820) { setCollapsed(false); setNavOpen((o) => !o); }
    else { setCollapsed((c) => !c); }
  };

  // autenticação
  const [user, setUser] = React.useState(null);
  const [authChecked, setAuthChecked] = React.useState(false);
  const [authMsg, setAuthMsg] = React.useState(''); // aviso na tela de login (ex.: empresa suspensa)

  // carregamento inicial
  const [loading, setLoading] = React.useState(true);
  const [loadError, setLoadError] = React.useState(null);

  // dados (espelho do banco, atualizado a cada resposta da API)
  const [appts, setAppts] = React.useState([]);
  const [clients, setClients] = React.useState([]);
  const [companies, setCompanies] = React.useState([]);
  const [suppliers, setSuppliers] = React.useState([]);
  const [services, setServices] = React.useState([]);
  const [team, setTeam] = React.useState([]);
  const [tasks, setTasks] = React.useState([]);
  const [transactions, setTransactions] = React.useState([]);
  const [notifications, setNotifications] = React.useState([]);
  const [historyClientId, setHistoryClientId] = React.useState('all');
  const [acknowledgedAppointmentIds, setAcknowledgedAppointmentIds] = React.useState([]);
  const [floatingNotifications, setFloatingNotifications] = React.useState([]);
  const floatingSeenRef = React.useRef(new Set((() => {
    try { return JSON.parse(sessionStorage.getItem('ag-floating-seen') || '[]'); }
    catch { return []; }
  })()));
  const dismissedFloatingRef = React.useRef(new Set());
  const [settings, setSettings] = React.useState({});

  const queueFloatingNotifications = React.useCallback((list, currentAppts = null) => {
    const todayStr = window.DB.iso(new Date());
    const now = new Date();
    const nowStr = String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
    
    const allFresh = (list || []).filter((n) => {
      if (n.read || floatingSeenRef.current.has(n.id)) return false;
      if (n.apptId && currentAppts) {
        const appt = currentAppts.find(a => a.id === n.apptId);
        if (!appt) return false;
        if (appt.showNotification === false) return false;
        if (appt.date < todayStr) return false;
        if (appt.date === todayStr && appt.start && appt.start < nowStr) return false;
      }
      return true;
    });
    
    if (!allFresh.length) return;
    allFresh.forEach((n) => floatingSeenRef.current.add(n.id));
    sessionStorage.setItem('ag-floating-seen', JSON.stringify([...floatingSeenRef.current].slice(-200)));
    
    const fresh = allFresh.slice(0, 3);
    setFloatingNotifications((current) => [...current, ...fresh].slice(-3));
  }, []);

  // navegação contextual
  const [calPro, setCalPro] = React.useState('default');
  const [clientQ, setClientQ] = React.useState('');
  const [clientPro, setClientPro] = React.useState('all');
  const searchRef = React.useRef(null);
  const notifRef = React.useRef(null);
  const [calendarToolbarHost, setCalendarToolbarHost] = React.useState(null);
  const [calendarNewActionHost, setCalendarNewActionHost] = React.useState(null);

  // modais
  const [newAppt, setNewAppt] = React.useState(null);   // prefill obj ou null
  const [openAppt, setOpenAppt] = React.useState(null);
  const [notifOpen, setNotifOpen] = React.useState(false);
  const [push, toastNode] = window.useToasts();
  const safeUser = React.useCallback((payload) => (
    payload && typeof payload === 'object' && payload.user && typeof payload.user === 'object'
      ? payload.user
      : null
  ), []);

  const trialDaysLeft = React.useMemo(() => {
    if (!user || !user.isTrial || !user.trialEndsAt) return null;
    const end = new Date(user.trialEndsAt + 'T00:00:00');
    const now = new Date();
    end.setHours(0,0,0,0);
    now.setHours(0,0,0,0);
    const diff = Math.ceil((end - now) / 86400000);
    return diff >= 0 ? diff : null;
  }, [user]);

  // carrega os dados da empresa logada (bootstrap)
  const load = React.useCallback(async () => {
    setLoading(true); setLoadError(null);
    try {
      const d = await API.bootstrap();
      if (!d || typeof d !== 'object') throw new Error('Resposta invalida do servidor ao carregar os dados.');
      const bootUser = safeUser(d);
      if (bootUser) setUser(bootUser);
      setTeam(Array.isArray(d.team) ? d.team : []);
      setServices(Array.isArray(d.services) ? d.services : []);
      setCompanies(Array.isArray(d.companies) ? d.companies : []);
      setSuppliers(Array.isArray(d.suppliers) ? d.suppliers : []);
      setClients(Array.isArray(d.clients) ? d.clients : []);
      setAppts(Array.isArray(d.appts) ? d.appts : []);
      setTasks(Array.isArray(d.tasks) ? d.tasks : []);
      setTransactions(Array.isArray(d.transactions) ? d.transactions : []);
      setNotifications(Array.isArray(d.notifications) ? d.notifications : []);
      setAcknowledgedAppointmentIds(Array.isArray(d.acknowledgedAppointmentIds) ? d.acknowledgedAppointmentIds : []);
      queueFloatingNotifications(Array.isArray(d.notifications) ? d.notifications : [], Array.isArray(d.appts) ? d.appts : []);
      setSettings(d.settings && typeof d.settings === 'object' ? d.settings : {});
      if (d.settings && d.settings.business_type) setBusinessTypeState(d.settings.business_type);
    } catch (e) {
      if (e.status === 401) { setUser(null); }
      else { setLoadError(e.message); }
    }
    setLoading(false);
  }, [queueFloatingNotifications, safeUser]);

  // checa a sessão; se logado, carrega os dados; senão mostra o login
  const init = React.useCallback(async () => {
    setLoading(true); setLoadError(null);
    try {
      const me = await API.auth.me();
      const current = safeUser(me);
      if (!current) {
        setUser(null);
        setAuthChecked(true);
        setLoading(false);
        return;
      }
      const slug = window.TENANT_SLUG || '';
      if (slug && current.tenantSlug && current.tenantSlug !== slug) {
        // sessão ativa era de OUTRA empresa: encerra e mostra o login desta
        try { await API.auth.logout(); } catch { /* ignora */ }
        setUser(null);
        setAuthMsg('Você estava conectado em outra empresa. Entre com o acesso desta empresa.');
      } else if (!slug && current.tenantSlug) {
        // raiz sem endereço, mas com sessão ativa: leva ao endereço da empresa
        window.location.replace((window.BASE_PATH || '/') + current.tenantSlug);
        return;
      } else {
        setUser(current);
        await load();
      }
    } catch (e) {
      if (e.status === 401) { setUser(null); }
      else { setLoadError(e.message); }
    }
    setAuthChecked(true);
    setLoading(false);
  }, [load, safeUser]);
  React.useEffect(() => { init(); }, [init]);

  // login / logout (o login vale só para a empresa do endereço atual)
  const doLogin = async (login, pass, remember = false) => {
    setAuthMsg('');
    const r = await API.auth.login(window.TENANT_SLUG || '', login, pass, remember); // erro propaga p/ a tela de login
    const logged = safeUser(r);
    if (!logged) throw new Error('Resposta invalida do servidor ao entrar.');
    setLoading(true);
    setUser(logged);
    setBusinessTypeState(logged.businessType || 'geral');
    await load();
  };
  const doLogout = async () => {
    try { await API.auth.logout(); } catch { /* ignora */ }
    setUser(null);
    setRoute('dashboard');
    setNotifOpen(false);
    setFloatingNotifications([]);
    setNavOpen(false);
  };

  // Sessão encerrada pelo servidor (empresa suspensa, usuário desativado, logout em outra aba…)
  // Derruba a tela na hora e volta ao login com um aviso.
  const sessionEnded = React.useCallback((msg) => {
    setAuthMsg(msg || 'Sua sessão foi encerrada. Entre novamente.');
    setUser(null);
    setRoute('dashboard');
    setNotifOpen(false);
    setFloatingNotifications([]);
    setNavOpen(false);
  }, []);
  const SESSION_BLOCKED = 'Acesso bloqueado: sua empresa foi suspensa ou a sessão expirou.';

  // Qualquer 401 vindo de uma chamada de dados (não-login) encerra a sessão imediatamente.
  React.useEffect(() => {
    window.onAuthExpired = () => sessionEnded(SESSION_BLOCKED);
    return () => { window.onAuthExpired = null; };
  }, [sessionEnded]);

  // Sincronização curta: novos agendamentos, respostas e notificações aparecem
  // nas duas pontas sem recarregar a página.
  React.useEffect(() => {
    if (!user) return;
    // além de detectar suspensão (401), atualiza os limites do plano caso o
    // acesso sênior os altere com o cliente já com o sistema aberto.
    const check = async () => {
      try {
        const [me, notifResult, apptResult] = await Promise.all([
          API.auth.me(), API.notifications.list(), API.appts.list(),
        ]);
        const current = safeUser(me);
        if (!current) {
          const err = new Error('Sessao invalida.');
          err.status = 401;
          throw err;
        }
        // Aplica na hora qualquer alteração que o admin faça neste usuário (papel,
        // nome, vínculo de profissional, limites do plano) — sem precisar deslogar
        // e logar de novo. Mantém a mesma referência se nada relevante mudou, para
        // não re-renderizar à toa a cada 30s.
        setUser((prev) => {
          if (!prev) return current;
          const keys = ['role', 'name', 'login', 'proId', 'maxUsers', 'maxPros', 'hasTeam', 'hasGroupMeetings', 'hasAppointmentEmail', 'hasFinance', 'hasInternalChat', 'showBillingMenu', 'loginUserCount', 'tenantName', 'businessType', 'isTrial', 'isExpired', 'planName', 'paidUntil', 'trialEndsAt'];
          return keys.some((k) => prev[k] !== current[k]) ? current : prev;
        });
        if (notifResult && notifResult.notifications) {
          queueFloatingNotifications(notifResult.notifications, apptResult ? apptResult.appointments : null);
          setFloatingNotifications((items) => items.filter((item) => !dismissedFloatingRef.current.has(item.id) && notifResult.notifications.some((n) => n.id === item.id && !n.read)));
          const sig = (list) => list.map((n) => n.id + ':' + (n.read ? 1 : 0)).join(',');
          setNotifications((prev) => sig(prev) === sig(notifResult.notifications) ? prev : notifResult.notifications);
          setAcknowledgedAppointmentIds(notifResult.acknowledgedAppointmentIds || []);
        }
        if (apptResult && apptResult.appointments) {
          const sig = (list) => list.map((a) => [
            a.id, a.date, a.start, window.apptEnd(a), a.serviceId, a.clientId || '',
            a.clientName || '', a.clientLabel || '', a.proId, a.status, a.notes || '', a.showNotification === false ? 0 : 1,
            (a.participants || []).map((p) => p.proId + '-' + p.response + '-' + (p.responseAt || '')).join('|'),
          ].join(':')).join(',');
          setAppts((prev) => sig(prev) === sig(apptResult.appointments) ? prev : apptResult.appointments);
        }
      } catch (e) { if (e.status === 401) sessionEnded(SESSION_BLOCKED); }
    };
    const iv = setInterval(check, 5000);
    const onVis = () => { if (document.visibilityState === 'visible') check(); };
    document.addEventListener('visibilitychange', onVis);
    return () => { clearInterval(iv); document.removeEventListener('visibilitychange', onVis); };
  }, [user, sessionEnded, queueFloatingNotifications, safeUser]);

  React.useEffect(() => { document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('ag-theme', theme); }, [theme]);
  React.useEffect(() => { document.documentElement.setAttribute('data-accent', accent); localStorage.setItem('ag-accent', accent); }, [accent]);
  React.useEffect(() => { localStorage.setItem('ag-collapsed', collapsed ? '1' : '0'); }, [collapsed]);
  React.useEffect(() => { localStorage.setItem('ag-biz', businessType); }, [businessType]);
  React.useEffect(() => { localStorage.setItem('ag-route', route); }, [route]);
  React.useEffect(() => {
    if (user && user.role === 'user' && route !== 'calendario') {
      setRoute('calendario');
    } else if (user && route === 'mensalidade' && user.showBillingMenu === false) {
      setRoute('dashboard');
    }
  }, [user, route]);

  React.useEffect(() => {
    if (!notifOpen) return;
    const closeOutside = (e) => {
      if (notifRef.current && !notifRef.current.contains(e.target)) setNotifOpen(false);
    };
    const closeEscape = (e) => { if (e.key === 'Escape') setNotifOpen(false); };
    document.addEventListener('pointerdown', closeOutside);
    document.addEventListener('keydown', closeEscape);
    return () => {
      document.removeEventListener('pointerdown', closeOutside);
      document.removeEventListener('keydown', closeEscape);
    };
  }, [notifOpen]);

  // ⌘K / Ctrl+K foca a busca
  React.useEffect(() => {
    const h = (e) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); if (searchRef.current) searchRef.current.focus(); } };
    window.addEventListener('keydown', h);
    return () => window.removeEventListener('keydown', h);
  }, []);

  /* ---- aplica ao estado local as alterações retornadas pela API ---- */
  const apply = (res) => {
    if (!res) return;
    const replace = (list, item) => list.map((x) => (x.id === item.id ? item : x));
    if (res.appointments) setAppts(res.appointments);
    if (res.appts) setAppts((a) => [...a, ...res.appts]);
    if (res.appt) setAppts((a) => a.some((x) => x.id === res.appt.id) ? replace(a, res.appt) : [...a, res.appt]);
    if (res.removedApptId) setAppts((a) => a.filter((x) => x.id !== res.removedApptId));
    if (res.companies) setCompanies(res.companies);
    if (res.company) setCompanies((cs) => cs.some((c) => c.id === res.company.id) ? replace(cs, res.company) : [...cs, res.company].sort((a, b) => a.name.localeCompare(b.name)));
    if (res.removedCompanyId) setCompanies((cs) => cs.filter((c) => c.id !== res.removedCompanyId));
    if (res.suppliers) setSuppliers(res.suppliers);
    if (res.supplier) setSuppliers((ss) => ss.some((s) => s.id === res.supplier.id) ? replace(ss, res.supplier) : [res.supplier, ...ss].sort((a, b) => a.name.localeCompare(b.name)));
    if (res.removedSupplierId) setSuppliers((ss) => ss.filter((s) => s.id !== res.removedSupplierId));
    if (res.clients) setClients(res.clients);
    if (res.client) setClients((cs) => cs.some((c) => c.id === res.client.id) ? replace(cs, res.client) : [res.client, ...cs]);
    if (res.removedClientId) setClients((cs) => cs.filter((c) => c.id !== res.removedClientId));
    if (res.service) setServices((ss) => ss.some((s) => s.id === res.service.id) ? replace(ss, res.service) : [...ss, res.service]);
    if (res.removedServiceId) setServices((ss) => ss.filter((s) => s.id !== res.removedServiceId));
    if (res.pro) setTeam((ts) => ts.some((p) => p.id === res.pro.id) ? replace(ts, res.pro) : [...ts, res.pro]);
    if (res.removedProId) setTeam((ts) => ts.filter((p) => p.id !== res.removedProId));
    if (res.task) setTasks((ts) => ts.some((t) => t.id === res.task.id) ? replace(ts, res.task) : [res.task, ...ts]);
    if (res.removedTaskId) setTasks((ts) => ts.filter((t) => t.id !== res.removedTaskId));
    if (res.transactions) setTransactions((txs) => {
      const incoming = res.transactions || [];
      const ids = new Set(incoming.map((t) => t.id));
      return [...incoming, ...txs.filter((t) => !ids.has(t.id))];
    });
    if (res.transaction) setTransactions((txs) => txs.some((t) => t.id === res.transaction.id) ? replace(txs, res.transaction) : [res.transaction, ...txs]);
    if (res.removedTxId) setTransactions((txs) => txs.filter((t) => t.id !== res.removedTxId));
    if (res.removedTxIds) setTransactions((txs) => txs.filter((t) => !res.removedTxIds.includes(t.id)));
    if (res.notification) setNotifications((ns) => [res.notification, ...ns]);
    if (res.notifications) {
      setNotifications(res.notifications);
      setFloatingNotifications((items) => items.filter((item) => res.notifications.some((n) => n.id === item.id && !n.read)));
    }
    if (res.acknowledgedAppointmentIds) setAcknowledgedAppointmentIds(res.acknowledgedAppointmentIds);
  };

  // envolve ações disparadas "soltas" na UI: erro vira toast
  const act = (fn) => async (...args) => {
    try { return await fn(...args); }
    catch (e) { push(e.message || 'Erro inesperado', 'x'); }
  };

  /* ---- ações (todas via AJAX, sem reload) ---- */
  // usadas por modais: propagam o erro para o modal decidir (ex.: manter aberto)
  const dispatchAppointmentEmail = (job) => {
    if (!job || !job.appointmentId) return;
    const type = job.type === 'cancellation' ? 'cancellation' : 'confirmation';
    push(type === 'cancellation' ? 'Enviando e-mail de cancelamento...' : 'Enviando e-mail de confirmação...', 'info');
    API.appts.sendEmail(job.appointmentId, type)
      .then((res) => {
        apply(res || {});
        const info = (res && res.email) || {};
        const sent = Number(info.sent || 0);
        const total = Number(info.total || 0);
        if (total <= 0) {
          push('Nenhum destinatário com e-mail para avisar.', 'info');
          return;
        }
        const label = type === 'cancellation' ? 'cancelamento' : 'confirmação';
        push(`Confirmação de envio de e-mail de ${label}: ${sent}/${total} enviado${sent === 1 ? '' : 's'}.`, sent > 0 ? 'check' : 'x');
      })
      .catch((e) => push(e.message || `${terms.appt} salvo, mas não foi possível enviar o e-mail.`, 'x'));
  };

  const addAppt = async (f) => {
    const payload = {
      clientId: f.isGroup ? null : (f.clientId || null),
      clientName: f.isGroup ? '' : (f.clientName || ''),
      clientIds: f.clientIds || [],
      proId: f.proId,
      proIds: f.proIds || [],
      isGroup: !!f.isGroup,
      serviceId: f.serviceId,
      manualServiceName: f.manualServiceName || '',
      manualServicePrice: f.manualServicePrice === '' ? null : f.manualServicePrice,
      date: f.date,
      start: f.start,
      end: f.end,
      status: f.status,
      notes: f.notes,
      showNotification: f.showNotification !== false,
      launchFinance: f.launchFinance === true,
      financeDueDate: f.financeDueDate || '',
      repeat: f.repeat,
      repeatUntil: f.repeatUntil || '',
      repeatDays: f.repeatDays || {},
      scope: f.editScope || 'single',
      quotaAction: f.quotaAction || 'included',
      addReplacementCredit: f.addReplacementCredit === true,
    };
    if (!f.editId) {
      payload.sendEmail = f.sendEmail === true;
      payload.emailReminderMinutes = f.sendEmail === true ? Number(f.emailReminderMinutes || 1440) : 0;
    }
    if (!f.editId && f.repeat === 'mes') {
      payload.monthlyWeekendPolicy = f.monthlyWeekendPolicy || 'keep';
    }
    const res = f.editId ? await API.appts.update(f.editId, payload) : await API.appts.create(payload);
    apply(res);
    if (res.emailDispatch) dispatchAppointmentEmail(res.emailDispatch);
    if (res.skipped > 0) push(res.skipped + ' repetições puladas por conflito de horário', 'info');
    return res;
  };
  const addClient = async (d) => {
    const res = await API.clients.create(d);
    apply(res);
    return res.client;
  };
  const updateClient = async (id, d) => { apply(await API.clients.update(id, d)); };
  const anonymizeClient = async (id) => {
    const res = await API.clients.anonymize(id);
    apply(res);
    return res.client;
  };
  const addSupplier = async (d) => {
    const res = await API.suppliers.create(d);
    apply(res);
    return res.supplier;
  };
  const updateSupplier = async (id, d) => { apply(await API.suppliers.update(id, d)); };
  const deleteSupplier = async (id) => { apply(await API.suppliers.remove(id)); };
  const addCompany = async (d) => { apply(await API.companies.create(d)); };
  const updateCompany = async (id, d) => { apply(await API.companies.update(id, d)); };
  const addService = async (d) => {
    const res = await API.services.create(d);
    apply(res);
    return res.service;
  };
  const updateService = async (id, d) => { apply(await API.services.update(id, d)); };
  const addPro = async (d) => {
    const res = await API.team.create(d);
    apply(res);
    if (d.createAcesso) setUser((u) => u ? { ...u, loginUserCount: (u.loginUserCount || 1) + 1 } : u);
    return res.pro;
  };
  const updatePro = async (id, d) => {
    apply(await API.team.update(id, d));
    if (d.createAcesso) setUser((u) => u ? { ...u, loginUserCount: (u.loginUserCount || 1) + 1 } : u);
  };
  const addTransaction = async (d) => { apply(await API.transactions.create(d)); };
  const updateTransaction = async (id, d) => { apply(await API.transactions.update(id, d)); };

  // usadas inline: erro vira toast
  const setApptStatus = async (id, status, options = {}) => {
    const res = await API.appts.update(id, { status, ...options });
    apply(res);
    if (res.emailDispatch) dispatchAppointmentEmail(res.emailDispatch);
    return res;
  };
  const respondAppt = act(async (id, response, proId) => { apply(await API.appts.respond(id, response, proId)); });
  const deleteAppt = async (id, deleteFinance = false) => {
    const res = await API.appts.remove(id, deleteFinance);
    apply(res);
    return res;
  };
  const cancelClientFuture = async (clientId, seriesId = 0) => {
    const res = await API.appts.cancelFuture(clientId, seriesId);
    apply(res);
    return res;
  };
  const manageApptSeries = async (id, mode) => {
    const res = await API.appts.seriesAction(id, mode);
    apply(res);
    return res;
  };
  const manageApptOrphanFuture = async (id) => {
    const res = await API.appts.orphanFutureAction(id);
    apply(res);
    return res;
  };
  const confirmAllToday = act(async () => {
    const r = await API.appts.confirmToday();
    apply(r);
    push(r.updated > 0 ? r.updated + ' confirmados!' : 'Nenhum pendente hoje');
  });
  const deleteClient = act(async (id) => { apply(await API.clients.remove(id)); push(`${terms.client} excluído`); });
  const deleteCompany = act(async (id) => { apply(await API.companies.remove(id)); push('Empresa excluída'); });
  const deleteService = act(async (id) => { apply(await API.services.remove(id)); push(`${terms.service} excluído`); });
  const duplicateService = act(async (s) => {
    apply(await API.services.create({ name: s.name + ' (cópia)', cat: s.cat, dur: s.dur, price: s.price, desc: s.desc, planFrequency: s.planFrequency, planPeriod: s.planPeriod, commissionType: s.commissionType, commissionValue: s.commissionValue, commissionOverrides: s.commissionOverrides || [] }));
    push(`${terms.service} duplicado!`);
  });
  const deletePro = act(async (id) => { apply(await API.team.remove(id)); push(terms.pro + ' removido'); });
  const addTask = act(async (data) => { apply(await API.tasks.create(typeof data === 'string' ? { text: data } : data)); });
  const toggleTask = act(async (id, done) => { apply(await API.tasks.update(id, { done })); });
  const deleteTask = act(async (id) => { apply(await API.tasks.remove(id)); });
  const payTransaction = act(async (id, payment) => {
    const payload = typeof payment === 'string' ? { status: 'pago', method: payment } : { status: 'pago', ...(payment || {}) };
    apply(await API.transactions.update(id, payload));
    push('Pagamento registrado!');
  });
  const deleteTransaction = act(async (id) => { apply(await API.transactions.remove(id)); push('Lançamento excluído'); });

  // notificações: otimista (UI responde na hora, API em segundo plano)
  const markNotifRead = (id) => {
    setNotifications((ns) => ns.map((n) => (n.id === id ? { ...n, read: true } : n)));
    setFloatingNotifications((items) => items.filter((item) => item.id !== id));
    API.notifications.read(id).catch(() => {});
  };
  const markAllNotifs = () => {
    const pendingIds = [...pendingInvites, ...pendingMeetings].map((item) => item.apptId);
    setNotifications((ns) => ns.map((n) => ({ ...n, read: true })));
    setAcknowledgedAppointmentIds((ids) => [...new Set([...ids, ...pendingIds])]);
    setFloatingNotifications([]);
    API.notifications.readAll(pendingIds)
      .then((res) => {
        if (res.notifications) setNotifications(res.notifications);
        if (res.acknowledgedAppointmentIds) setAcknowledgedAppointmentIds(res.acknowledgedAppointmentIds);
      })
      .catch(() => {});
  };
  const dismissAppt = (apptId) => {
    setAcknowledgedAppointmentIds((ids) => [...new Set([...ids, apptId])]);
    API.notifications.readAll([apptId])
      .then((res) => {
        if (res.acknowledgedAppointmentIds) setAcknowledgedAppointmentIds(res.acknowledgedAppointmentIds);
      })
      .catch(() => {});
  };
  const deleteNotification = (id) => {
    setNotifications((ns) => ns.filter((n) => n.id !== id));
    setFloatingNotifications((items) => items.filter((item) => item.id !== id));
    API.notifications.remove(id).catch(() => {});
  };
  const openNotification = (notification) => {
    markNotifRead(notification.id);
    if (notification.apptId) {
      const appt = appts.find((item) => item.id === notification.apptId);
      if (appt) setOpenAppt(appt);
    }
    setNotifOpen(false);
    setFloatingNotifications((items) => items.filter((item) => item.id !== notification.id));
  };
  const closeFloatingNotification = (id) => {
    dismissedFloatingRef.current.add(id);
    setFloatingNotifications((items) => items.filter((item) => item.id !== id));
    markNotifRead(id);
  };
  // Ao abrir o sino: busca notificações + agenda na hora, para os pendentes
  // aparecerem imediatamente (sem esperar o ciclo de ~30s) e sempre coerentes.
  const refreshBell = async () => {
    try {
      const [n, a] = await Promise.all([API.notifications.list(), API.appts.list()]);
      if (n && n.notifications) setNotifications(n.notifications);
      if (n && n.acknowledgedAppointmentIds) setAcknowledgedAppointmentIds(n.acknowledgedAppointmentIds);
      if (a && a.appointments) setAppts(a.appointments);
    } catch { /* silencioso */ }
  };
  const openNotif = () => {
    if (!notifOpen) refreshBell();
    setNotifOpen((prev) => !prev);
    if (!notifOpen) setFloatingNotifications([]);
  };
  const showNotifications = () => {
    refreshBell();
    setNotifOpen(true);
  };
  // confirma/recusa um convite direto pelo sino (sem abrir o agendamento)
  const respondInvite = async (apptId, response, notifId) => {
    try {
      apply(await API.appts.respond(apptId, response, user && user.proId));
      // marca o convite como lido (apaga o ponto do sino) — pelo id recebido ou
      // localizando a notificação daquele compromisso.
      const nid = notifId || (notifications.find((n) => n.type === 'invite' && n.apptId === apptId) || {}).id;
      if (nid) markNotifRead(nid);
      push(response === 'confirmado' ? 'Presença confirmada' : 'Convite recusado');
    } catch (e) {
      push(e.message || 'Erro ao responder o convite', 'x');
    }
  };

  const saveSetting = async (k, v) => {
    setSettings((s) => ({ ...s, [k]: v }));
    return API.settings.set(k, v);
  };
  const saveClientCategories = async (categories) => {
    const value = JSON.stringify(categories || []);
    const res = await API.settings.set('client_categories', value);
    setSettings((current) => ({ ...current, client_categories: (res && res.value) || value }));
    return categories;
  };
  const terms = DB.businessTypes[businessType];
  const notifTodayISO = DB.iso(DB.today);
  const notifLimitISO = DB.iso(DB.addDays(5));
  const notificationApptById = new Map(appts.map((a) => [a.id, a]));
  const notificationCutoff = Date.now() - 5 * 24 * 60 * 60 * 1000;
  const notificationIsRecent = (n) => {
    if (n.type === 'pending_read') return false;
    if (n.apptId) {
      const appt = notificationApptById.get(n.apptId);
      if (!appt) return false;
      if (appt.showNotification === false) return false;
      if (appt.date < notifTodayISO || appt.date > notifLimitISO) return false;
      if (appt.date === notifTodayISO && appt.start) {
        const now = new Date();
        const nowStr = String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
        if (appt.start < nowStr) return false;
      }
      return true;
    }
    const timestamp = new Date(String(n.createdAt || '').replace(' ', 'T')).getTime();
    return Number.isFinite(timestamp) && timestamp >= notificationCutoff;
  };
  const unread = notifications.filter((n) => !n.read && notificationIsRecent(n)).length;
  const pendingTasks = tasks.filter((t) => !t.done).length;

  // Pendentes derivados da agenda (fonte da verdade — sempre coerente, não some
  // ao marcar notificação como lida). Aparecem fixos no topo do sino.
  const todayISO = DB.iso(DB.today);
  // Evita que recorrências anuais inundem o sino com pendências distantes.
  const attentionUntilISO = DB.iso(DB.addDays(5));
  const myProId = (user && user.proId) ? user.proId : 0;
  const apptLabel = (a) => {
    const c = clients.find((x) => x.id === a.clientId);
    const s = a.manualServiceName ? { name: a.manualServiceName, price: a.manualServicePrice || 0 } : services.find((x) => x.id === a.serviceId);
    const svcName = s ? s.name : terms.appt;
    const clientLabel = a.clientLabel || a.clientName || (c ? c.name : '');
    return clientLabel ? (clientLabel + ' — ' + svcName) : svcName;
  };
  const apptWhen = (a) => {
    const d = new Date(a.date + 'T00:00');
    return d.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' }) + ' · ' + a.start;
  };
  const upcomingAppt = (a) => {
    if (a.status === 'cancelado' || a.showNotification === false) return false;
    if (a.date < todayISO || a.date > attentionUntilISO) return false;
    if (a.date === todayISO && a.start) {
      const now = new Date();
      const nowStr = String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
      if (a.start < nowStr) return false;
    }
    return true;
  };
  // convites que EU (profissional) preciso confirmar
  const allPendingInvites = myProId
    ? appts.filter((a) => upcomingAppt(a) && (a.participants || []).some((p) => p.proId === myProId && p.response === 'pendente'))
        .map((a) => ({ apptId: a.id, title: apptLabel(a), sub: apptWhen(a) }))
    : [];
  const acknowledgedSet = new Set(acknowledgedAppointmentIds);
  const pendingInvites = allPendingInvites.filter((item) => !acknowledgedSet.has(item.apptId));
  // reuniões com outros participantes ainda não confirmados (visão de quem marcou)
  const pendingMeetings = appts
    .filter((a) => upcomingAppt(a) && a.createdByUserId === (user && user.id) && !allPendingInvites.some((pi) => pi.apptId === a.id))
    .map((a) => ({
      apptId: a.id, title: apptLabel(a), sub: apptWhen(a),
      names: (a.participants || []).filter((p) => p.response === 'pendente' && p.proId !== myProId).map((p) => p.name),
    }))
    .filter((it) => it.names.length > 0 && !acknowledgedSet.has(it.apptId));
  const attentionKeys = new Set();
  notifications.filter((n) => !n.read && notificationIsRecent(n)).forEach((n) => attentionKeys.add(n.apptId ? `appt-${n.apptId}` : `notif-${n.id}`));
  pendingInvites.forEach((item) => attentionKeys.add(`appt-${item.apptId}`));
  pendingMeetings.forEach((item) => attentionKeys.add(`appt-${item.apptId}`));
  const attentionCount = attentionKeys.size;
  const attentionText = pendingInvites.length > 0
    ? `${pendingInvites.length} compromisso${pendingInvites.length === 1 ? '' : 's'} aguardando sua confirmação`
    : pendingMeetings.length > 0
      ? `${pendingMeetings.length} compromisso${pendingMeetings.length === 1 ? '' : 's'} aguardando respostas`
      : `${unread} notificação${unread === 1 ? '' : 'ões'} não lida${unread === 1 ? '' : 's'}`;
  // limites do plano definidos pelo acesso sênior (0 = ilimitado); vêm no objeto user
  const limits = { maxUsers: (user && (user.maxUsers ?? user.maxAdmins)) || 0, maxPros: (user && user.maxPros) || 0 };

  const ctx = {
    user, logout: doLogout, limits,
    terms, appts, clients, companies, suppliers, services, team, tasks, transactions, notifications, settings,
    statusMap: DB.statusMap, money: DB.money,
    calPro, clientQ, clientPro, calendarToolbarHost, calendarNewActionHost,
    historyClientId,
    openNewAppt: (prefill = {}) => {
      if (user && (user.role === 'pro' || user.role === 'user') && user.proId) {
        const requestedPro = prefill.proId && team.some((p) => p.id === prefill.proId) ? prefill.proId : user.proId;
        setNewAppt({ ...prefill, proId: requestedPro });
      } else {
        setNewAppt(prefill);
      }
    },
    openAppt: (a) => setOpenAppt(a),
    toast: push,
    theme, setTheme, accent, setAccent, businessType, saveSetting, saveClientCategories,
    go: (r) => {
      const target = user && user.showBillingMenu === false && r === 'mensalidade' ? 'dashboard' : r;
      setCalPro(target === 'calendario' ? 'default' : 'all');
      if (target === 'historico') setHistoryClientId('all');
      setClientQ('');
      setClientPro('all');
      setRoute(user && user.role === 'user' ? 'calendario' : target);
      setNavOpen(false);
    },
    openCalendarFor: (proId) => { setCalPro(user && (user.role === 'pro' || user.role === 'user') ? user.proId : proId); setRoute('calendario'); },
    openClientsFor: (proId) => { setClientPro(proId); setRoute('clientes'); },
    searchClients: (q) => { setClientQ(q); setClientPro('all'); setRoute('clientes'); },
    openClientHistory: (clientId) => { setHistoryClientId(Number(clientId)); setRoute('historico'); setNavOpen(false); },
    addAppt, setApptStatus, respondAppt, deleteAppt, cancelClientFuture, manageApptSeries, manageApptOrphanFuture, confirmAllToday,
    addClient, updateClient, anonymizeClient, deleteClient, addSupplier, updateSupplier, deleteSupplier,
    addCompany, updateCompany, deleteCompany,
    addService, updateService, deleteService, duplicateService,
    addPro, updatePro, deletePro,
    addTask, toggleTask, deleteTask,
    addTransaction, updateTransaction, payTransaction, deleteTransaction,
    markNotifRead, markAllNotifs,
    updateCurrentUser: (u) => setUser((prev) => ({ ...prev, ...u })),
  };

  if (loadError) {
    return (
      <div className="boot">
        <div className="boot-card">
          <window.Icon name="info" size={30} style={{ color: 'var(--danger)' }} />
          <h3 style={{ margin: '10px 0 2px' }}>Não foi possível carregar</h3>
          <div className="muted" style={{ fontSize: 13, marginBottom: 16 }}>{loadError}</div>
          <window.Button variant="primary" onClick={init}>Tentar novamente</window.Button>
        </div>
      </div>
    );
  }
  if (!authChecked || loading) {
    return (
      <div className="boot">
        <div className="boot-card"><div className="spinner" /><div>Carregando sua agenda…</div></div>
      </div>
    );
  }
  if (!user) {
    if (window.TENANT_SLUG === 'acesso-gratuito') {
      return <window.FreeTrialScreen />;
    }
    return <window.LoginScreen onLogin={doLogin} notice={authMsg} />;
  }

  const nav = user.role === 'user' ? [
    { group: 'Principal', items: [
      { id: 'calendario', label: 'Calendário', icon: 'calendar' },
    ]},
  ] : [
    { group: 'Principal', items: [
      { id: 'dashboard', label: 'Dashboard', icon: 'dashboard' },
      { id: 'calendario', label: 'Calendário', icon: 'calendar' },
    ]},
    { group: 'Gestão', items: [
      { id: 'clientes', label: terms.clients, icon: 'users' },
      ...(user.hasFinance && (user.hasTeam || user.role === 'admin') ? [{ id: 'fornecedores', label: 'Fornecedores', icon: 'building' }] : []),
      { id: 'servicos', label: terms.services, icon: 'tag' },
      ...((user.hasTeam || user.role === 'admin') ? [{ id: 'equipe', label: terms.pros, icon: 'team' }] : []),
    ]},
    { group: 'Operação', items: [
      ...(user.hasFinance && user.isPrimaryAdmin ? [{ id: 'financeiro', label: 'Financeiro', icon: 'wallet' }] : []),
      ...(user.hasInternalChat ? [{ id: 'chat', label: 'Chat interno', icon: 'mail' }] : []),
      { id: 'tarefas', label: 'Tarefas', icon: 'check2', badge: pendingTasks },
      { id: 'historico', label: 'Histórico', icon: 'history' },
    ]},
    ...(user.role === 'admin' && user.isPrimaryAdmin ? [{ group: 'Administração', items: [
      { id: 'usuarios', label: 'Usuários', icon: 'team' },
      ...(user.showBillingMenu !== false ? [{ id: 'mensalidade', label: 'Mensalidade', icon: 'wallet' }] : []),
    ]}] : []),
  ];

  const titles = {
    dashboard: ['Dashboard', 'Visão geral do seu dia'],
    calendario: ['Calendário', 'Gerencie todos os ' + terms.appt.toLowerCase() + 's'],
    clientes: [terms.clients, 'Base de ' + terms.clients.toLowerCase()],
    fornecedores: ['Fornecedores', 'Cadastro e consulta de fornecedores'],
    servicos: [terms.services, 'Catálogo e preços'],
    equipe: [terms.pros, terms.pros + ' e disponibilidade'],
    financeiro: ['Financeiro', 'Receitas, despesas e pagamentos'],
    chat: ['Chat interno', 'Comunicação da equipe'],
    tarefas: ['Tarefas', 'Lembretes e pendências'],
    historico: ['Histórico', terms.appt + 's anteriores'],
    usuarios: ['Usuários', 'Acessos da sua empresa'],
    mensalidade: ['Mensalidade', 'Plano e pagamentos da sua empresa'],
    config: ['Configurações', 'Preferências'],
  };
  const tenantPlanLabel = user.showBillingMenu === false ? 'Conta ativa' : (user.planName ? 'Plano ' + user.planName : (user.isTrial ? 'Período de teste' : 'Plano ativo'));

  const visibleRoute = route === 'mensalidade' && user.showBillingMenu === false ? 'dashboard' : route;
  const isCalendarRoute = visibleRoute === 'calendario';

  const renderScreen = () => {
    const screens = {
      dashboard: window.Dashboard,
      calendario: window.CalendarScreen,
      clientes: window.ClientsScreen,
      fornecedores: window.SuppliersScreen,
      servicos: window.ServicesScreen,
      equipe: window.TeamScreen,
      financeiro: window.FinanceScreen,
      chat: InternalChatScreen,
      tarefas: window.TasksScreen,
      historico: window.HistoryScreen,
      usuarios: window.UsersScreen,
      mensalidade: window.BillingScreen,
      config: window.SettingsScreen,
    };
    if (user && user.role === 'user' && route !== 'calendario') {
      return React.createElement(window.CalendarScreen, { key: 'calendario', ...ctx });
    }
    // só admin principal acessa a gestão de usuários e a mensalidade
    if ((visibleRoute === 'usuarios' || visibleRoute === 'mensalidade') && (!user || user.role !== 'admin' || !user.isPrimaryAdmin)) {
      return React.createElement(window.Dashboard, { key: 'dashboard', ...ctx });
    }
    // empresa sem equipe: admins ainda acessam para ajustar o próprio perfil/horário.
    if (visibleRoute === 'equipe' && user && !user.hasTeam && user.role !== 'admin') {
      return React.createElement(window.Dashboard, { key: 'dashboard', ...ctx });
    }
    if (visibleRoute === 'financeiro' && user && (!user.hasFinance || !user.isPrimaryAdmin)) {
      return React.createElement(window.Dashboard, { key: 'dashboard', ...ctx });
    }
    if (visibleRoute === 'fornecedores' && user && (!user.hasFinance || (!user.hasTeam && user.role !== 'admin'))) {
      return React.createElement(window.Dashboard, { key: 'dashboard', ...ctx });
    }
    if (visibleRoute === 'chat' && user && !user.hasInternalChat) {
      return React.createElement(window.Dashboard, { key: 'dashboard', ...ctx });
    }
    const Comp = screens[visibleRoute];
    return Comp ? React.createElement(Comp, { key: visibleRoute, ...ctx }) : React.createElement(MissingScreen, { key: visibleRoute });
  };

  const [t1, t2] = titles[visibleRoute] || ['', ''];

  return (
    <div className={'app' + (collapsed ? ' collapsed' : '') + (navOpen ? ' nav-open' : '')}>
      {/* SIDEBAR */}
      <aside className="sidebar">
        <div className="sb-brand">
          <div className="sb-logo"><img src="assets/del-agenda-icon.png" alt="" /></div>
          <div className="name">DEL AGENDA<small>{terms.name}</small></div>
        </div>

        <div className="sb-switch" style={{ cursor: 'default' }} title={user.tenantName || terms.name}>
          <div className="ic"><window.Icon name={terms.icon} size={17} /></div>
          <div className="lbl"><b>{user.tenantName || terms.name}</b><span>{tenantPlanLabel}</span></div>
        </div>

        <nav className="sb-nav">
          {nav.map(grp => (
            <React.Fragment key={grp.group}>
              <div className="sb-section">{grp.group}</div>
              {grp.items.map(it => (
                <button key={it.id} className={'nav-item' + (visibleRoute === it.id ? ' active' : '')} onClick={() => ctx.go(it.id)} title={it.label}>
                  <span className="ico"><window.Icon name={it.icon} size={18} /></span>
                  <span className="t">{it.label}</span>
                  {it.badge > 0 && <span className="badge-count">{it.badge}</span>}
                </button>
              ))}
            </React.Fragment>
          ))}
        </nav>

        <div className="sb-foot">
          {user.role !== 'user' && <button className={'nav-item' + (visibleRoute === 'config' ? ' active' : '')} onClick={() => ctx.go('config')} title="Configurações">
            <span className="ico"><window.Icon name="settings" size={18} /></span><span className="t">Configurações</span>
          </button>}
          <button type="button" className="sb-user" title="Desconectar" aria-label="Desconectar" onClick={doLogout}>
            <window.Avatar initials={(user.name || user.login || 'U').slice(0, 2).toUpperCase()} color={user.color || 'gray'} size={34} />
            <div className="meta"><b>{user.name || user.login}</b><span>{user.role === 'admin' ? 'Administrador' : (user.role === 'pro' ? terms.pro || 'Profissional' : 'Usuário')}</span></div>
            <span className="iconbtn logout-icon" aria-hidden="true" style={{ width: 30, height: 30 }}>
              <window.Icon name="logout" size={16} style={{ color: 'var(--text-3)' }} />
            </span>
          </button>
        </div>
      </aside>

      {/* fundo da gaveta (mobile) */}
      <div className="nav-backdrop" onClick={() => setNavOpen(false)} />

      {/* MAIN */}
      <div className="main">
        <header className={'topbar' + (isCalendarRoute ? ' calendar-topbar' : '')}>
          <button className="iconbtn" onClick={toggleMenu} title="Menu"><window.Icon name="list" size={19} /></button>
          {isCalendarRoute ? (
            <div className="calendar-topbar-host" ref={setCalendarToolbarHost} />
          ) : (<>
            <div className="page-h">
              <h1>{t1}</h1>
              <p>{t2}</p>
            </div>
            <div className="spacer" />
            {user.role !== 'user' && <div className="search">
              <window.Icon name="search" size={16} />
              <input ref={searchRef} placeholder={'Buscar ' + terms.clients.toLowerCase() + '…'}
                onKeyDown={(e) => {
                  if (e.key === 'Enter' && e.target.value.trim()) {
                    ctx.searchClients(e.target.value.trim());
                    e.target.value = ''; e.target.blur();
                  }
                }} />
              <kbd>⌘K</kbd>
            </div>}
            <button className="iconbtn bordered" onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} title="Tema">
              <window.Icon name={theme === 'dark' ? 'sun' : 'moon'} size={18} />
            </button>
            <button className="iconbtn bordered" onClick={() => {
              if (window.caches) {
                caches.keys().then(names => names.forEach(name => caches.delete(name)));
              }
              window.location.reload(true);
            }} title="Atualizar sistema (Limpar Cache)">
              <window.Icon name="refresh" size={18} />
            </button>
          </>)}
          <div ref={notifRef} style={{ position: 'relative' }}>
            <button className="iconbtn bordered notification-bell" onClick={openNotif}
              title={`${attentionCount} notificações e pendências`}
              aria-label={`Notificações: ${attentionCount} ${attentionCount === 1 ? 'aviso' : 'avisos'}`}>
              <window.Icon name="bell" size={18} />{attentionCount > 0 && <span className="notification-count">{attentionCount > 99 ? '99+' : attentionCount}</span>}
            </button>
            {notifOpen && (
              <div style={{ position: 'absolute', right: 0, top: '100%', zIndex: 60 }}>
                <window.NotifPanel notifications={notifications} appts={appts} pendingInvites={pendingInvites} pendingMeetings={pendingMeetings} onRead={openNotification} onMarkRead={markNotifRead} onReadAll={markAllNotifs} onRespond={respondInvite} onDismissAppt={dismissAppt} onDelete={deleteNotification} onClose={() => setNotifOpen(false)} />
              </div>
            )}
          </div>
          {isCalendarRoute
            ? <div className="calendar-new-action-host" ref={setCalendarNewActionHost} />
            : <window.Button variant="primary" icon="plus" onClick={() => ctx.openNewAppt({})}>Novo</window.Button>}
        </header>

        {user.isExpired && (
          <div className="notification-strip" role="alert" style={{ backgroundColor: '#fee2e2', color: '#991b1b', borderColor: '#f87171' }}>
            <div className="notification-strip-icon"><window.Icon name="info" size={17} /></div>
            <div className="notification-strip-copy">
              <b style={{ color: '#991b1b' }}>{user.showBillingMenu === false ? 'Acesso em modo leitura' : 'Seu período de teste expirou'}</b>
              <span style={{ color: '#991b1b' }}>{user.showBillingMenu === false ? 'Apenas leitura permitida. Fale com o suporte para voltar a editar.' : 'Apenas leitura permitida. Assine um plano no menu Mensalidade para voltar a editar.'}</span>
            </div>
            {user.role === 'admin' && user.isPrimaryAdmin && user.showBillingMenu !== false && (
              <button className="btn sm outline" onClick={() => ctx.go('mensalidade')} style={{ color: '#991b1b', borderColor: '#991b1b' }}>Ver Planos</button>
            )}
          </div>
        )}
        
        {!user.isExpired && user.showBillingMenu !== false && trialDaysLeft !== null && trialDaysLeft <= 10 && (
          <div className="notification-strip" role="alert">
            <div className="notification-strip-icon"><window.Icon name="info" size={17} /></div>
            <div className="notification-strip-copy">
              <b>Período de teste quase no fim</b>
              <span>Seu acesso expira {trialDaysLeft === 0 ? 'hoje' : `em ${trialDaysLeft} dia${trialDaysLeft === 1 ? '' : 's'}`}. Assine um plano para não ser bloqueado.</span>
            </div>
          </div>
        )}

        {attentionCount > 0 && (
          <div className="notification-strip" role="status">
            <div className="notification-strip-icon"><window.Icon name="bell" size={17} /></div>
            <div className="notification-strip-copy">
              <b>Atenção necessária</b>
              <span>{attentionText}</span>
            </div>
            <button className="btn sm outline" onClick={showNotifications}>Ver avisos</button>
          </div>
        )}

        <main className="content">{renderScreen()}</main>
      </div>

      {/* MODAIS */}
      {newAppt !== null && <window.NewApptModal ctx={ctx} prefill={newAppt} onClose={() => setNewAppt(null)} />}
      {openAppt && <window.ApptDrawer ctx={ctx} appt={appts.find(a => a.id === openAppt.id) || openAppt} onClose={() => setOpenAppt(null)} />}
      {floatingNotifications.length > 0 && (
        <div className="floating-notification-stack" aria-live="polite">
          {floatingNotifications.map((notification) => (
            <div key={notification.id} className="floating-notification" role="alert" onClick={() => openNotification(notification)}>
              <div className="floating-notification-icon"><window.Icon name={notification.icon || 'bell'} size={17} /></div>
              <div className="floating-notification-copy">
                <b>{notification.title}</b>
                <span>{notification.desc}</span>
              </div>
              <button className="iconbtn" title="Fechar" aria-label="Fechar notificação"
                onClick={(e) => { e.stopPropagation(); closeFloatingNotification(notification.id); }}>
                <window.Icon name="x" size={15} />
              </button>
            </div>
          ))}
        </div>
      )}
      {toastNode}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
