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

/* Endereço do proxy já publicado desta congregação. Fica embutido no app
   pra quem usa não precisar saber o que é um "Worker" — só entra com a
   senha na tela de login. Dá pra trocar em Ajustes se um dia mudar de
   servidor. */
const DEFAULT_PROXY_URL = "https://designacoes-proxy.designacoes-ls.workers.dev";

/* Chave pública do alerta por notificação (Web Push/VAPID) — não é
   segredo, o navegador manda ela pro serviço de push na hora de se
   inscrever. A privada correspondente fica só no Worker (secret VAPID_JWK). */
const VAPID_PUBLIC_KEY =
  "BENb5rb5S6JiWunTdI6dN8c6jLbWmqcJA1Vx9Zt7gwFOqVep1QAj6t7uvq6xelp_dYMWSxvRBDwwGd0xveaikII";

function urlBase64ToUint8Array(base64url) {
  const padding = "=".repeat((4 - (base64url.length % 4)) % 4);
  const base64 = (base64url + padding).replace(/-/g, "+").replace(/_/g, "/");
  const raw = atob(base64);
  const bytes = new Uint8Array(raw.length);
  for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
  return bytes;
}

/* ---------------------------------------------------------
   Configuração local do aparelho — endereço do proxy + senha (chave de
   acesso). Isso NÃO sincroniza entre aparelhos: fica guardado só aqui,
   depois do login, pra manter a sessão sem pedir senha de novo.
--------------------------------------------------------- */
const LOCAL_CONFIG_KEY = "designacoes:local-config";
const localDeviceConfig = {
  read() {
    try {
      const v = localStorage.getItem(LOCAL_CONFIG_KEY);
      const parsed = v ? JSON.parse(v) : {};
      return {
        proxyUrl: parsed.proxyUrl || DEFAULT_PROXY_URL,
        syncSecret: parsed.syncSecret || "",
      };
    } catch {
      return { proxyUrl: DEFAULT_PROXY_URL, syncSecret: "" };
    }
  },
  write(cfg) {
    try {
      localStorage.setItem(LOCAL_CONFIG_KEY, JSON.stringify(cfg));
    } catch {}
  },
};

/* Id da inscrição de notificação (Web Push) deste navegador — local, não
   sincroniza (cada instalação do app tem a sua própria inscrição). */
const PUSH_ID_KEY = "designacoes:push-sub-id";
const getLocalPushId = () => {
  try {
    return localStorage.getItem(PUSH_ID_KEY) || "";
  } catch {
    return "";
  }
};
const setLocalPushId = (id) => {
  try {
    if (id) localStorage.setItem(PUSH_ID_KEY, id);
    else localStorage.removeItem(PUSH_ID_KEY);
  } catch {}
};

/* ---------------------------------------------------------
   Armazenamento compartilhado (nuvem) — fala com o proxy/Worker, que
   guarda tudo num KV da Cloudflare. Assim os dados (pessoas, semanas,
   histórico) aparecem iguais em qualquer aparelho configurado com o
   mesmo endereço + chave de acesso, em vez de ficar só no navegador.
--------------------------------------------------------- */
const storage = {
  _proxyUrl: "",
  _secret: "",
  configure({ proxyUrl, syncSecret }) {
    this._proxyUrl = (proxyUrl || "").trim().replace(/\/+$/, "");
    this._secret = syncSecret || "";
  },
  async call(path, body) {
    if (!this._proxyUrl) throw new Error("SEM_SERVIDOR");
    const res = await fetch(this._proxyUrl + path, {
      method: "POST",
      headers: { "Content-Type": "application/json", "x-app-secret": this._secret },
      body: JSON.stringify(body),
    });
    let data = null;
    try {
      data = await res.json();
    } catch {}
    if (!res.ok) throw new Error((data && data.error) || `Falha no servidor (${res.status})`);
    return data || {};
  },
  async get(key) {
    try {
      const data = await this.call("/kv/get", { key });
      return data.value != null ? { value: data.value } : null;
    } catch {
      return null;
    }
  },
  async set(key, value) {
    try {
      await this.call("/kv/set", { key, value });
    } catch {}
  },
  async delete(key) {
    try {
      await this.call("/kv/delete", { key });
    } catch {}
  },
  async keys() {
    try {
      const data = await this.call("/kv/list", { prefix: "" });
      return Array.isArray(data.keys) ? data.keys : [];
    } catch {
      return [];
    }
  },
};

/* ---------------------------------------------------------
   Tipos de parte e agrupamento por seção / cor
--------------------------------------------------------- */
const PART_TYPES = [
  { id: "presidente", label: "Presidente" },
  { id: "video", label: "Vídeo" },
  { id: "camera_indicador", label: "Câmera/Indicador" },
  { id: "oracao", label: "Oração" },
  { id: "tesouros_discurso", label: "Discurso dos Tesouros", section: "tesouros" },
  { id: "joias", label: "Joias espirituais", section: "tesouros" },
  { id: "leitura_biblia", label: "Leitura da Bíblia", section: "tesouros" },
  { id: "iniciando_conversas", label: "Iniciando conversas", section: "ministerio" },
  { id: "cultivando_interesse", label: "Cultivando o interesse", section: "ministerio" },
  { id: "fazendo_discipulos", label: "Fazendo discípulos", section: "ministerio" },
  { id: "explicando_crencas", label: "Explicando suas crenças", section: "ministerio" },
  { id: "discurso_estudante", label: "Discurso do estudante", section: "ministerio" },
  { id: "o_que_voce_diria", label: "O que você diria?", section: "ministerio" },
  { id: "vida_crista", label: "Vida Cristã (parte local)", section: "vida" },
  { id: "estudo_biblico_conducao", label: "Dirigente do estudo bíblico", section: "vida" },
];
const TYPE_BY_ID = Object.fromEntries(PART_TYPES.map((t) => [t.id, t]));

/* Semanas importadas antes de um novo typeId existir no código (ex.: "O que
   você diria?", adicionado depois) ficam com o typeId antigo (geralmente
   "vida_crista", o fallback da importação) salvo nos dados. Corrige isso ao
   carregar, comparando o título, pra essas semanas passarem a usar as
   qualificações certas sem precisar reimportar o PDF. */
const LEGACY_TITLE_TYPE_FIXES = { "o que você diria?": "o_que_voce_diria" };
function fixPartsTypeIds(parts) {
  if (!Array.isArray(parts)) return parts;
  return parts.map((part) => {
    const wanted = LEGACY_TITLE_TYPE_FIXES[(part.title || "").trim().toLowerCase()];
    return wanted && part.typeId !== wanted ? { ...part, typeId: wanted } : part;
  });
}
function fixEntriesTypeIds(entries) {
  if (!Array.isArray(entries)) return entries;
  return entries.map((e) => (e.parts ? { ...e, parts: fixPartsTypeIds(e.parts) } : e));
}

const SECTIONS = {
  tesouros: { label: "Tesouros da Palavra de Deus", color: "#2B5D66" },
  ministerio: { label: "Faça seu melhor no Ministério", color: "#9C7A28" },
  vida: { label: "Nossa Vida Cristã", color: "#8B3A3A" },
  geral: { label: "", color: "#4A4740" },
};
const sectionOf = (typeId) => TYPE_BY_ID[typeId]?.section || "geral";

const uid = () =>
  (crypto.randomUUID && crypto.randomUUID()) ||
  `${Date.now()}-${Math.random().toString(16).slice(2)}`;

const todayISO = () => new Date().toISOString().slice(0, 10);
const addDaysISO = (iso, days) => {
  const d = new Date(iso + "T00:00:00");
  d.setDate(d.getDate() + days);
  return d.toISOString().slice(0, 10);
};

/* ---------------------------------------------------------
   Agrupamento de semanas por mês (aba Semanas)
--------------------------------------------------------- */
const MONTH_NAMES = [
  "Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho",
  "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro",
];
const monthLabelOf = (iso) => {
  if (!iso || !/^\d{4}-\d{2}/.test(iso)) return "Sem data";
  const [y, m] = iso.split("-");
  const name = MONTH_NAMES[parseInt(m, 10) - 1] || "Mês";
  return `${name} de ${y}`;
};
const groupEntriesByMonth = (list) => {
  const groups = [];
  const byLabel = new Map();
  for (const e of list) {
    const label = monthLabelOf(e.date);
    if (!byLabel.has(label)) {
      const g = { label, entries: [] };
      byLabel.set(label, g);
      groups.push(g);
    }
    byLabel.get(label).entries.push(e);
  }
  return groups;
};

const SYSTEM_PROMPT_BIMESTER = `Você extrai dados de um PDF com a programação de VÁRIAS semanas (normalmente um bimestre) da Reunião Vida e Ministério Cristão (Testemunhas de Jeová).

Devolva SOMENTE um objeto JSON, sem markdown, sem comentários, sem texto antes ou depois, neste formato:
{"weeks": [{"weekLabel": "<data e leitura da semana, como aparece no PDF>", "date": "<data do primeiro dia (segunda-feira) da semana, em AAAA-MM-DD, ou null se não der pra determinar>", "noMeeting": <true|false>, "reason": "<motivo, só quando noMeeting=true, ex: 'Assembleia de Circuito'>", "parts": [{"number": <inteiro ou null>, "title": "<título original da parte, sem o número>", "minutes": <inteiro ou null>, "typeId": "<um id da lista abaixo>", "needsAssistant": <true|false>}]}]}

IDs válidos para "typeId" (use exatamente estes): presidente, video, camera_indicador, oracao, tesouros_discurso, joias, leitura_biblia, iniciando_conversas, cultivando_interesse, fazendo_discipulos, explicando_crencas, discurso_estudante, o_que_voce_diria, vida_crista, estudo_biblico_conducao.

Regras:
- Inclua UMA entrada em "weeks" para cada semana do período abrangido pelo PDF, na ordem em que aparecem — inclusive semanas sem programação de reunião.
- Se uma semana não tiver reunião (assembleia, congresso, Celebração sem partes, feriado, etc.), marque "noMeeting": true, preencha "reason" com o motivo tal como está no PDF, e deixe "parts": [].
- Para semanas com reunião, "parts" segue estas regras:
  - A apostila NÃO imprime "Presidente", "Vídeo" nem "Câmera/Indicador" como itens
    numerados, e as orações de abertura e encerramento aparecem junto com o cântico
    (ex.: "Cântico 1 e oração", "Cântico 143 e oração") em vez de como itens separados.
    Mesmo assim, TODA semana com reunião tem essas partes, e você deve gerá-las por conta
    própria, com "number": null, nesta ordem, como os 4 primeiros itens da lista:
    1) typeId "presidente" (título "Presidente");
    2) typeId "video" (título "Vídeo");
    3) typeId "camera_indicador" (título "Câmera/Indicador");
    4) a oração inicial, typeId "oracao" (título "Oração", referente ao cântico/oração do
       início, logo depois de "Comentários iniciais").
    E, como ÚLTIMO item da lista (depois de todos os itens numerados, inclusive depois do
    estudo bíblico de congregação), mais um item typeId "oracao" (título "Oração",
    referente ao cântico/oração que aparece junto com "Comentários finais").
  - Entre a oração inicial e a oração final, inclua os itens numerados (1., 2., 3.…) na
    ordem em que aparecem, seguindo as regras abaixo.
  - "Estudo bíblico de congregação" vira UM item, typeId "estudo_biblico_conducao" (só tem dirigente, não usamos leitor separado).
  - needsAssistant = true para as partes de demonstração da seção "Faça seu melhor no Ministério" (normalmente iniciando conversas, cultivando o interesse, fazendo discípulos, explicando suas crenças), a não ser que o título deixe claro que é um discurso individual do estudante. needsAssistant = false para todas as outras.
  - NÃO inclua cânticos, "comentários iniciais" nem "comentários finais" como itens.
  - Se não conseguir identificar o typeId de alguma parte, ainda assim inclua o item, usando o typeId que mais se aproximar.
- Em "date", use o ano que aparece no PDF para calcular a data. Se não conseguir determinar com confiança, use null.`;

/* ---------------------------------------------------------
   Lê um arquivo local e devolve { mediaType, base64 }
--------------------------------------------------------- */
function fileToBase64(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      const match = /^data:(.*);base64,(.*)$/.exec(reader.result || "");
      if (!match) {
        reject(new Error("Arquivo inválido."));
        return;
      }
      resolve({ mediaType: match[1], base64: match[2] });
    };
    reader.onerror = () => reject(new Error("Falha ao ler o arquivo."));
    reader.readAsDataURL(file);
  });
}

/* Deixa claro, no título, qual é a oração inicial e qual é a final — o
   modelo devolve o mesmo título ("Oração") pras duas ocorrências, então
   ajustamos aqui em vez de confiar nisso vir certo da IA. */
function refineSyntheticTitles(parts) {
  const oracaoIdx = [];
  parts.forEach((p, i) => {
    if (p.typeId === "oracao") oracaoIdx.push(i);
  });
  if (oracaoIdx.length >= 2) {
    parts[oracaoIdx[0]].title = "Oração inicial";
    parts[oracaoIdx[oracaoIdx.length - 1]].title = "Oração final";
  } else if (oracaoIdx.length === 1) {
    parts[oracaoIdx[0]].title = "Oração";
  }
}

/* ---------------------------------------------------------
   Extração de todas as semanas de um PDF de bimestre, via proxy -> Gemini
--------------------------------------------------------- */
async function extractBimesterFromProxy({ file, proxyUrl }) {
  if (!proxyUrl || !proxyUrl.trim()) {
    throw new Error("SEM_SERVIDOR");
  }

  const response = await fetch(proxyUrl.trim().replace(/\/+$/, ""), {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      system: SYSTEM_PROMPT_BIMESTER,
      userText: "Extraia todas as semanas deste PDF, seguindo as instruções.",
      file: { mediaType: file.mediaType, base64: file.base64 },
    }),
  });

  if (!response.ok) {
    let detail = "";
    try {
      detail = (await response.json())?.error || "";
    } catch {}
    throw new Error(detail || `Falha na API (${response.status})`);
  }

  const data = await response.json();
  const text = (data.text || "").trim();
  const clean = text.replace(/```json|```/g, "").trim();
  const parsed = JSON.parse(clean);
  if (!parsed || !Array.isArray(parsed.weeks)) throw new Error("Formato inesperado");

  return parsed.weeks.map((w) => {
    const parts = Array.isArray(w.parts)
      ? w.parts.map((p) => ({
          id: uid(),
          number: p.number ?? null,
          title: p.title || TYPE_BY_ID[p.typeId]?.label || "Parte",
          minutes: p.minutes ?? null,
          typeId: TYPE_BY_ID[p.typeId] ? p.typeId : "vida_crista",
          needsAssistant: !!p.needsAssistant,
        }))
      : [];
    refineSyntheticTitles(parts);
    return {
      weekLabel: w.weekLabel || "",
      date: typeof w.date === "string" && /^\d{4}-\d{2}-\d{2}$/.test(w.date) ? w.date : "",
      noMeeting: !!w.noMeeting,
      reason: w.reason || "",
      parts,
    };
  });
}

/* ---------------------------------------------------------
   Algoritmo de rodízio (com peso para membros não efetivos)
--------------------------------------------------------- */
const REGULAR_WEIGHT = 2; // efetivo recebe até 2x mais designações que não efetivo
const weightOf = (p) => (p.efetivo === false ? 1 : REGULAR_WEIGHT);

const daysBetween = (fromISO, toISO) => (new Date(toISO) - new Date(fromISO)) / 86400000;

// Depois de formar uma dupla no Ministério, os dois ficam impedidos de
// repetir a mesma dupla (mesmo invertendo quem é o principal/ajudante) por
// esse número de dias — cobre a reunião seguinte mesmo que as datas variem
// um pouco, e libera a dupla de novo depois disso.
const PARTNER_COOLDOWN_DAYS = 10;

function urgency(person, typeId, referenceDate) {
  const last = person.lastAssigned?.[typeId];
  const days = last ? daysBetween(last, referenceDate) : 9999;
  return days * weightOf(person);
}

function pickCandidate(people, typeId, usedIds, referenceDate) {
  // "rotationExempt" é pra opções tipo "Stream"/gravação: aparecem pra
  // seleção manual, mas nunca são sugeridas automaticamente.
  const eligible = people.filter(
    (p) => p.qualifications.includes(typeId) && !usedIds.has(p.id) && !p.rotationExempt
  );
  eligible.sort((a, b) => {
    const ua = urgency(a, typeId, referenceDate);
    const ub = urgency(b, typeId, referenceDate);
    return ua !== ub ? ub - ua : a.name.localeCompare(b.name);
  });
  return eligible[0] || null;
}

/* Regras de rodízio numa mesma semana (com base no padrão real da
   congregação): dentro de cada SEÇÃO (Tesouros da Palavra de Deus, Faça seu
   melhor no Ministério, Nossa Vida Cristã — esta incluindo o Estudo bíblico
   de congregação) ninguém repete, mas uma pessoa pode acumular uma parte de
   cada seção diferente na mesma semana (ex.: um discurso de Tesouros +
   uma parte de Vida Cristã).
   - Presidente é livre (não conta em nenhuma seção) e é escolhido primeiro,
     porque a oração inicial e a exclusão de vídeo/câmera dependem dele.
   - Vídeo e Câmera/Indicador não podem ser a mesma pessoa entre si, e
     também não podem ser quem estiver como Presidente nem quem estiver
     dirigindo o Estudo bíblico naquela semana.
   - A oração inicial é sempre a mesma pessoa do Presidente; a oração
     final não pode ser a mesma pessoa da inicial.
   - Vídeo, Câmera/Indicador e Oração só usam membros efetivos. */
const EFETIVO_ONLY_TYPES = new Set(["video", "camera_indicador", "oracao"]);

/* Partes que podem ser feitas por "interpretação" (qualquer irmão, mesmo
   sem estar habilitado para elas no cadastro): a 1ª parte de Tesouros da
   Palavra de Deus e as partes de Nossa Vida Cristã — com exceção do Estudo
   bíblico de congregação e da Oração. */
const INTERPRETAR_TYPE_IDS = new Set(["tesouros_discurso", "vida_crista"]);

/* "existing" traz as designações já preenchidas (por importação anterior,
   edição manual, ou um recálculo anterior). Uma designação já preenchida
   NUNCA é sobrescrita — só as que estão em branco (null) são calculadas.
   Pra recalcular uma parte específica, deixe-a em branco (selecione
   "— Selecionar —" de novo) antes de clicar em Recalcular sugestões. */
function suggestAssignments(parts, people, referenceDate, existing) {
  const refDate = referenceDate || todayISO();
  const efetivos = people.filter((p) => p.efetivo !== false);
  const already = existing || {};
  const result = {};

  const sectionUsed = { tesouros: new Set(), ministerio: new Set(), vida: new Set() };
  const videoCameraUsed = new Set();
  const oracaoUsed = new Set();

  // Presidente é resolvido primeiro, independente da ordem no array — a
  // oração inicial e a exclusão de vídeo/câmera dependem dele.
  const presidenteIdx = parts.findIndex((p) => p.typeId === "presidente");
  let presidenteId = null;
  if (presidenteIdx !== -1) {
    const part = parts[presidenteIdx];
    const fixedMain = already[part.id]?.main || null;
    const main = fixedMain || pickCandidate(people, part.typeId, new Set(), refDate)?.id || null;
    presidenteId = main;
    result[part.id] = { main: presidenteId, assistant: null };
  }
  if (presidenteId) {
    videoCameraUsed.add(presidenteId);
    oracaoUsed.add(presidenteId);
  }

  // Estudo bíblico (dirigente) também é resolvido antes do vídeo/câmera —
  // e ocupa a vaga da seção "Vida Cristã" pra essa pessoa.
  const estudoIdx = parts.findIndex((p) => p.typeId === "estudo_biblico_conducao");
  if (estudoIdx !== -1) {
    const part = parts[estudoIdx];
    const fixedMain = already[part.id]?.main || null;
    const main = fixedMain || pickCandidate(people, part.typeId, sectionUsed.vida, refDate)?.id || null;
    if (main) sectionUsed.vida.add(main);
    result[part.id] = { main, assistant: null };
    if (main) videoCameraUsed.add(main);
  }

  // A primeira "Oração" da semana (a inicial) é sempre o presidente.
  const initialOracaoIdx = parts.findIndex((p) => p.typeId === "oracao");

  parts.forEach((part, i) => {
    if (i === presidenteIdx || i === estudoIdx) return; // já resolvidos acima

    if (i === initialOracaoIdx) {
      const fixedMain = already[part.id]?.main || null;
      result[part.id] = { main: fixedMain || presidenteId, assistant: null };
      return;
    }

    let used;
    let pool;
    if (part.typeId === "video" || part.typeId === "camera_indicador") {
      used = videoCameraUsed;
      pool = efetivos;
    } else if (part.typeId === "oracao") {
      used = oracaoUsed;
      pool = efetivos;
    } else {
      // Tesouros / Ministério / Vida Cristã: uma vaga por seção por semana.
      const sec = sectionOf(part.typeId);
      used = sectionUsed[sec] || new Set();
      pool = people;
    }

    const fixed = already[part.id] || {};
    let mainId = fixed.main || null;
    if (!mainId) {
      const main = pickCandidate(pool, part.typeId, used, refDate);
      mainId = main?.id || null;
    }
    if (mainId) used.add(mainId);

    let assistantId = fixed.assistant || null;
    if (part.needsAssistant && !assistantId) {
      // No Ministério, ajudante tem que ser do mesmo sexo do estudante, e
      // dois ocasionais nunca formam dupla — se o estudante é ocasional,
      // o ajudante tem que ser efetivo.
      const mainPerson = pool.find((p) => p.id === mainId);
      let assistantPool = pool;
      if (mainPerson && mainPerson.gender) {
        assistantPool = assistantPool.filter((p) => p.gender === mainPerson.gender);
      }
      if (mainPerson && mainPerson.efetivo === false) {
        assistantPool = assistantPool.filter((p) => p.efetivo !== false);
      }
      // Não repete a mesma dupla que já foi formada recentemente (nem
      // invertendo quem é principal/ajudante).
      if (mainPerson) {
        assistantPool = assistantPool.filter((p) => {
          const lastPaired = mainPerson.lastPartner?.[p.id] || p.lastPartner?.[mainPerson.id];
          if (!lastPaired) return true;
          return daysBetween(lastPaired, refDate) >= PARTNER_COOLDOWN_DAYS;
        });
      }
      const assistant = pickCandidate(assistantPool, part.typeId, used, refDate);
      assistantId = assistant?.id || null;
    }
    if (assistantId) used.add(assistantId);

    result[part.id] = {
      main: mainId,
      assistant: part.needsAssistant ? assistantId : null,
      interpretar: fixed.interpretar || false,
    };
  });

  return result;
}

/** Gera sugestões para várias semanas em sequência, respeitando o que já
 *  estiver preenchido e considerando que quem já foi designado numa semana
 *  anterior do mesmo lote deve ceder vez na próxima. */
function suggestForBatch(entries, people, startDate) {
  let virtual = people.map((p) => ({
    ...p,
    lastAssigned: { ...(p.lastAssigned || {}) },
    lastPartner: { ...(p.lastPartner || {}) },
  }));
  const out = {};
  let dayOffset = 0;
  for (const entry of entries) {
    if (entry.mode === "sem_reuniao" || !entry.parts || entry.parts.length === 0) continue;
    const refDate = entry.date || addDaysISO(startDate, dayOffset);
    const assignments = suggestAssignments(entry.parts, virtual, refDate, entry.assignments);
    out[entry.id] = assignments;
    dayOffset += 7;
    virtual = virtual.map((p) => {
      let touched = false;
      const nextLast = { ...p.lastAssigned };
      const nextPartner = { ...p.lastPartner };
      for (const part of entry.parts) {
        const a = assignments[part.id];
        if (a.main === p.id || a.assistant === p.id) {
          nextLast[part.typeId] = refDate;
          touched = true;
        }
        if (part.needsAssistant && a.main && a.assistant) {
          if (a.main === p.id) {
            nextPartner[a.assistant] = refDate;
            touched = true;
          } else if (a.assistant === p.id) {
            nextPartner[a.main] = refDate;
            touched = true;
          }
        }
      }
      return touched ? { ...p, lastAssigned: nextLast, lastPartner: nextPartner } : p;
    });
  }
  return out;
}

const newEntry = (mode = "sem_reuniao") => ({
  id: uid(),
  mode, // 'semana' | 'sem_reuniao'
  date: "",
  skipReason: "",
  weekTitle: "",
  parts: [],
  assignments: {},
});

/* ---------------------------------------------------------
   Componente principal
--------------------------------------------------------- */
function DesignacoesApp() {
  const [tab, setTab] = useState("semanas");
  const [people, setPeople] = useState([]);
  const [peopleLoaded, setPeopleLoaded] = useState(false);

  // Endereço do proxy + chave de acesso: config LOCAL do aparelho (não
  // sincroniza — é o que diz pro aparelho onde buscar os dados na nuvem).
  const [localConfig, setLocalConfig] = useState(() => localDeviceConfig.read());
  const proxyUrl = localConfig.proxyUrl;
  // Rascunho dos campos da aba Ajustes — só vira "de verdade" (localConfig)
  // quando clica em "Salvar e conectar", pra não reconectar a cada tecla.
  const [connectDraft, setConnectDraft] = useState(() => localDeviceConfig.read());

  // Nome da congregação e config do alerta: compartilhados (nuvem), igual
  // pessoas e semanas.
  const [cloudConfig, setCloudConfig] = useState({
    congName: "",
    reminderEnabled: false,
    reminderDays: 7,
  });
  const congName = cloudConfig.congName;
  const [cloudLoaded, setCloudLoaded] = useState(false);
  const [syncStatus, setSyncStatus] = useState("");

  const [pushStatus, setPushStatus] = useState("");
  const [pushBusy, setPushBusy] = useState(false);

  const [rotationBusy, setRotationBusy] = useState(false);
  const [rotationMsg, setRotationMsg] = useState("");

  const [importMsg, setImportMsg] = useState("");
  const backupInputRef = useRef(null);

  const [entries, setEntries] = useState([]);
  const [importError, setImportError] = useState("");
  const [importing, setImporting] = useState(false);
  const [importOkMsg, setImportOkMsg] = useState("");
  const [openMonthLabel, setOpenMonthLabel] = useState(null);
  const [recalcMsg, setRecalcMsg] = useState("");

  /* Confirmação genérica pra qualquer remover/excluir do app. */
  const [confirmDialog, setConfirmDialog] = useState(null); // { message, confirmLabel, onConfirm }
  const askConfirm = (message, onConfirm, confirmLabel) =>
    setConfirmDialog({ message, onConfirm, confirmLabel: confirmLabel || "Excluir" });
  const [openEntryId, setOpenEntryId] = useState(null);
  const [monthSaved, setMonthSaved] = useState(false);
  const [saveError, setSaveError] = useState("");

  const [history, setHistory] = useState([]);
  const [historyLoaded, setHistoryLoaded] = useState(false);
  const [viewingWeek, setViewingWeek] = useState(null);
  const [weekEditing, setWeekEditing] = useState(false);
  const [weekSaveMsg, setWeekSaveMsg] = useState("");

  const [newName, setNewName] = useState("");
  const [newGender, setNewGender] = useState("M");
  const bimesterInputRef = useRef(null);

  const [printData, setPrintData] = useState(null);
  const [printSubtitle, setPrintSubtitle] = useState("");
  const printRequested = useRef(false);

  /* ---- Carregar dados da nuvem (pessoas, config, histórico, rascunho) ---- */
  const loadCloudData = useCallback(async () => {
    try {
      const r = await storage.get("people");
      setPeople(r ? JSON.parse(r.value) : []);
    } catch {
      setPeople([]);
    }
    setPeopleLoaded(true);
    try {
      const r = await storage.get("config");
      if (r) {
        const cfg = JSON.parse(r.value);
        setCloudConfig({
          congName: cfg.congName || "",
          reminderEnabled: !!cfg.reminderEnabled,
          reminderDays: cfg.reminderDays || 7,
        });
      }
    } catch {}
    try {
      const r = await storage.get("weeks-index");
      setHistory(r ? JSON.parse(r.value) : []);
    } catch {
      setHistory([]);
    }
    setHistoryLoaded(true);
    try {
      const r = await storage.get("month-draft");
      if (r) {
        const draft = JSON.parse(r.value);
        if (Array.isArray(draft.entries)) setEntries(fixEntriesTypeIds(draft.entries));
        if (typeof draft.monthSaved === "boolean") setMonthSaved(draft.monthSaved);
      }
    } catch {}
    setCloudLoaded(true);
  }, []);

  useEffect(() => {
    storage.configure(localConfig);
    if (localConfig.syncSecret) loadCloudData(); // sem senha salva, mostra a tela de login
    // Só na montagem, com o que já estava salvo neste aparelho — reconectar
    // de propósito (outro endereço/chave) passa por connectCloud().
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  /* As semanas ainda não salvas (o "rascunho" do mês) ficam guardadas
     automaticamente na nuvem, pra aparecerem iguais em qualquer aparelho e
     não sumirem se o app for recarregado/atualizado antes de você clicar
     em "Confirmar e salvar o mês". Espera meio segundo sem mudanças antes
     de gravar, pra não mandar um pedido a cada tecla digitada. */
  useEffect(() => {
    if (!cloudLoaded) return;
    const t = setTimeout(() => {
      storage.set("month-draft", JSON.stringify({ entries, monthSaved }));
    }, 800);
    return () => clearTimeout(t);
  }, [entries, monthSaved, cloudLoaded]);

  /* O nome da congregação também é compartilhado entre aparelhos. */
  useEffect(() => {
    if (!cloudLoaded) return;
    const t = setTimeout(() => {
      storage.set("config", JSON.stringify(cloudConfig));
    }, 800);
    return () => clearTimeout(t);
  }, [cloudConfig, cloudLoaded]);

  const savePeople = useCallback(async (next) => {
    setPeople(next);
    try {
      await storage.set("people", JSON.stringify(next));
    } catch {}
  }, []);

  const saveCloudConfig = useCallback((patch) => {
    setCloudConfig((prev) => ({ ...prev, ...patch }));
  }, []);

  /* Login: só grava a senha/endereço no aparelho e entra de fato se a
     senha estiver correta (o teste de conexão precisa dar certo antes).
     Uma senha errada nunca fica salva nem "loga" o usuário. */
  const handleLogin = async (password) => {
    const proxyUrl = (localConfig.proxyUrl || DEFAULT_PROXY_URL).trim();
    const attempt = { proxyUrl, syncSecret: password };
    storage.configure(attempt);
    await storage.call("/kv/get", { key: "__ping__" }); // joga erro se a senha/endereço estiverem errados

    localDeviceConfig.write(attempt);
    setLocalConfig(attempt);
    setCloudLoaded(false);
    await loadCloudData();
  };

  const handleLogout = () => {
    const cleared = { proxyUrl: localConfig.proxyUrl || DEFAULT_PROXY_URL, syncSecret: "" };
    localDeviceConfig.write(cleared);
    setLocalConfig(cleared);
    storage.configure(cleared);
    setPeople([]);
    setPeopleLoaded(false);
    setHistory([]);
    setHistoryLoaded(false);
    setEntries([]);
    setMonthSaved(false);
    setCloudConfig({ congName: "", reminderEnabled: false, reminderDays: 7 });
    setCloudLoaded(false);
    setOpenMonthLabel(null);
    setOpenEntryId(null);
    setViewingWeek(null);
    setTab("semanas");
  };

  /* ---- Alerta por notificação (Web Push) ---- */
  const subscribeToPush = async () => {
    if (!("serviceWorker" in navigator) || !("PushManager" in window) || !("Notification" in window)) {
      setPushStatus("Este navegador não tem suporte a notificações.");
      return false;
    }
    setPushBusy(true);
    setPushStatus("");
    try {
      const permission = await Notification.requestPermission();
      if (permission !== "granted") {
        setPushStatus("Permissão de notificação negada — ative nas configurações do navegador.");
        return false;
      }
      const registration = await navigator.serviceWorker.ready;
      let sub = await registration.pushManager.getSubscription();
      if (!sub) {
        sub = await registration.pushManager.subscribe({
          userVisibleOnly: true,
          applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
        });
      }
      const id = getLocalPushId() || (crypto.randomUUID ? crypto.randomUUID() : uid());
      setLocalPushId(id);
      await storage.call("/push/subscribe", { id, subscription: sub.toJSON() });
      setPushStatus("Notificações ativadas neste aparelho.");
      return true;
    } catch (e) {
      setPushStatus("Não consegui ativar: " + (e && e.message ? e.message : "erro desconhecido"));
      return false;
    } finally {
      setPushBusy(false);
    }
  };

  const unsubscribeFromPush = async () => {
    setPushBusy(true);
    try {
      const id = getLocalPushId();
      if (id) {
        try {
          await storage.call("/push/unsubscribe", { id });
        } catch {}
      }
      setLocalPushId("");
      if ("serviceWorker" in navigator) {
        const registration = await navigator.serviceWorker.ready;
        const sub = await registration.pushManager.getSubscription();
        if (sub) await sub.unsubscribe();
      }
      setPushStatus("Notificações desativadas neste aparelho.");
    } finally {
      setPushBusy(false);
    }
  };

  const handleReminderToggle = async (checked) => {
    if (checked) {
      const ok = await subscribeToPush();
      if (ok) saveCloudConfig({ reminderEnabled: true, reminderDays: cloudConfig.reminderDays || 7 });
    } else {
      await unsubscribeFromPush();
      saveCloudConfig({ reminderEnabled: false });
    }
  };

  const sendTestPush = async () => {
    setPushBusy(true);
    setPushStatus("Enviando teste…");
    try {
      const res = await storage.call("/push/test", {});
      setPushStatus(`Teste enviado (${res.sent || 0} inscrição${res.sent === 1 ? "" : "ões"}).`);
    } catch (e) {
      setPushStatus("Não consegui enviar o teste: " + (e && e.message ? e.message : "erro desconhecido"));
    } finally {
      setPushBusy(false);
    }
  };

  /* Endereço do proxy fica só neste aparelho. Só reconectamos (buscamos
     os dados da nuvem de novo) quando o usuário clica em "Salvar" —
     nunca a cada tecla digitada. */
  const connectCloud = async (nextLocalConfig) => {
    localDeviceConfig.write(nextLocalConfig);
    setLocalConfig(nextLocalConfig);
    storage.configure(nextLocalConfig);
    setCloudLoaded(false);
    if (!nextLocalConfig.proxyUrl) {
      setSyncStatus("");
      setCloudLoaded(true);
      return;
    }
    setSyncStatus("Conectando…");
    try {
      await storage.call("/kv/get", { key: "__ping__" });
      setSyncStatus("Conectado — dados sincronizados neste aparelho.");
    } catch (e) {
      setSyncStatus(
        "Não consegui conectar: " + (e && e.message ? e.message : "confira o endereço e a chave.")
      );
    }
    await loadCloudData();
  };

  /* ---- Backup (exportar / importar) ---- */
  const exportBackup = async () => {
    const dump = { app: "designacoes", version: 1, exportedAt: new Date().toISOString(), data: {} };
    for (const key of await storage.keys()) {
      const r = await storage.get(key);
      if (r) dump.data[key] = r.value;
    }
    const blob = new Blob([JSON.stringify(dump, null, 2)], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `designacoes-backup-${todayISO()}.json`;
    document.body.appendChild(a);
    a.click();
    a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  };

  const importBackup = async (file) => {
    if (!file) return;
    setImportMsg("");
    try {
      const dump = JSON.parse(await file.text());
      if (!dump || typeof dump.data !== "object" || dump.data === null) throw new Error("bad");
      for (const [key, value] of Object.entries(dump.data)) {
        if (typeof value === "string") await storage.set(key, value);
      }
      const p = await storage.get("people");
      setPeople(p ? JSON.parse(p.value) : []);
      const c = await storage.get("config");
      if (c) {
        const cfg = JSON.parse(c.value);
        setCloudConfig({
          congName: cfg.congName || "",
          reminderEnabled: !!cfg.reminderEnabled,
          reminderDays: cfg.reminderDays || 7,
        });
      } else {
        setCloudConfig({ congName: "", reminderEnabled: false, reminderDays: 7 });
      }
      const h = await storage.get("weeks-index");
      setHistory(h ? JSON.parse(h.value) : []);
      const d = await storage.get("month-draft");
      if (d) {
        const draft = JSON.parse(d.value);
        setEntries(Array.isArray(draft.entries) ? fixEntriesTypeIds(draft.entries) : []);
        setMonthSaved(typeof draft.monthSaved === "boolean" ? draft.monthSaved : false);
      }
      setViewingWeek(null);
      setImportMsg("Backup restaurado com sucesso.");
    } catch {
      setImportMsg("Não consegui ler este arquivo de backup.");
    }
  };

  /* ---- Pessoas ---- */
  const addPerson = () => {
    const name = newName.trim();
    if (!name) return;
    savePeople([
      ...people,
      { id: uid(), name, gender: newGender, qualifications: [], lastAssigned: {}, lastPartner: {}, efetivo: true },
    ]);
    setNewName("");
  };
  const removePerson = (id) => savePeople(people.filter((p) => p.id !== id));
  const renamePerson = (id, name) =>
    savePeople(people.map((p) => (p.id !== id ? p : { ...p, name })));
  const toggleQualification = (personId, typeId) => {
    savePeople(
      people.map((p) =>
        p.id !== personId
          ? p
          : {
              ...p,
              qualifications: p.qualifications.includes(typeId)
                ? p.qualifications.filter((q) => q !== typeId)
                : [...p.qualifications, typeId],
            }
      )
    );
  };
  const toggleEfetivo = (personId) => {
    savePeople(
      people.map((p) => (p.id !== personId ? p : { ...p, efetivo: p.efetivo === false ? true : false }))
    );
  };
  const setGender = (personId, gender) => {
    savePeople(people.map((p) => (p.id !== personId ? p : { ...p, gender })));
  };
  const toggleRotationExempt = (personId) => {
    savePeople(
      people.map((p) => (p.id !== personId ? p : { ...p, rotationExempt: !p.rotationExempt }))
    );
  };

  /* ---- Gerenciar entradas do mês ---- */
  const updateEntry = (id, patch) =>
    setEntries((prev) => prev.map((e) => (e.id === id ? { ...e, ...patch } : e)));

  const addWeekEntry = () => {
    setEntries((prev) => [...prev, newEntry("sem_reuniao")]);
    setMonthSaved(false);
  };

  const removeEntry = (id) => {
    setEntries((prev) => prev.filter((e) => e.id !== id));
    setOpenEntryId((cur) => (cur === id ? null : cur));
    setMonthSaved(false);
  };

  const toggleSemReuniao = (id) => {
    setEntries((prev) =>
      prev.map((e) =>
        e.id !== id ? e : { ...e, mode: e.mode === "sem_reuniao" ? "semana" : "sem_reuniao" }
      )
    );
    setMonthSaved(false);
  };

  /* ---- Importar PDF do bimestre ----
     Cada importação SOMA à lista atual: semanas com a mesma data substituem a
     versão anterior (útil se você importar o mesmo arquivo de novo), semanas
     com data nova são adicionadas, e tudo fica ordenado por data. Assim dá
     pra importar vários arquivos (bimestres diferentes) um atrás do outro. */
  const importBimesterPdf = async (file) => {
    if (!file) return;
    setImporting(true);
    setImportError("");
    setImportOkMsg("");
    try {
      const fileData = await fileToBase64(file);
      const weeks = await extractBimesterFromProxy({ file: fileData, proxyUrl });
      if (weeks.length === 0) throw new Error("VAZIO");

      // Só estrutura a programação (semanas e partes) — nenhum nome é
      // sugerido aqui. As sugestões só acontecem quando você clicar em
      // "Recalcular sugestões", depois de revisar as semanas e marcar
      // eventos (congresso, assembleia etc.), se houver.
      const readyEntries = weeks.map((w) => ({
        id: uid(),
        mode: w.noMeeting ? "sem_reuniao" : "semana",
        date: w.date || "",
        skipReason: w.reason || "",
        weekTitle: w.weekLabel || (w.noMeeting ? "Sem reunião" : "Semana"),
        parts: w.noMeeting ? [] : w.parts,
        assignments: {},
      }));

      setEntries((prev) => {
        const byDate = new Map();
        const noDate = [];
        for (const e of prev) {
          if (e.date) byDate.set(e.date, e);
          else noDate.push(e);
        }
        for (const e of readyEntries) {
          if (e.date) byDate.set(e.date, e);
          else noDate.push(e);
        }
        const dated = Array.from(byDate.values()).sort((a, b) =>
          a.date < b.date ? -1 : a.date > b.date ? 1 : 0
        );
        return [...dated, ...noDate];
      });
      setMonthSaved(false);
      setImportOkMsg(
        `✅ ${readyEntries.length} semana${readyEntries.length > 1 ? "s" : ""} importada${
          readyEntries.length > 1 ? "s" : ""
        } com sucesso! Revise as semanas abaixo e marque eventos (se houver) antes de recalcular as sugestões.`
      );
    } catch (e) {
      let msg = "Não consegui ler esse PDF. Confira o arquivo e tente de novo.";
      if (e && e.message === "SEM_SERVIDOR") {
        msg = "Configure o endereço do servidor na aba Ajustes antes de importar.";
      } else if (e && e.message === "VAZIO") {
        msg = "O modelo não encontrou nenhuma semana nesse PDF. Tente importar de novo.";
      } else if (e && e.message && e.message !== "Formato inesperado") {
        msg = "Não consegui ler esse PDF: " + e.message;
      }
      setImportError(msg);
    } finally {
      setImporting(false);
    }
  };

  /* Recalcula as sugestões só das semanas do mês informado, e só preenche
     designações em branco — o que já estiver preenchido não é alterado.
     Pra recalcular uma parte específica, deixe-a em branco antes (escolha
     "— Selecionar —" de novo) e clique aqui de novo. */
  const recalcMonthSuggestions = (monthLabel) => {
    const monthEntries = entries.filter((e) => monthLabelOf(e.date) === monthLabel);
    if (monthEntries.length === 0) return;
    const start = monthEntries.find((e) => e.date)?.date || todayISO();
    const suggestions = suggestForBatch(monthEntries, people, start);

    let filledCount = 0;
    for (const e of monthEntries) {
      const after = suggestions[e.id];
      if (!after || !e.parts) continue;
      for (const part of e.parts) {
        const before = e.assignments[part.id] || {};
        const now = after[part.id] || {};
        if (!before.main && now.main) filledCount++;
        if (part.needsAssistant && !before.assistant && now.assistant) filledCount++;
      }
    }

    setEntries((prev) =>
      prev.map((e) => (suggestions[e.id] ? { ...e, assignments: suggestions[e.id] } : e))
    );
    setMonthSaved(false);
    setRecalcMsg(
      filledCount > 0
        ? `✅ Sugestões recalculadas com sucesso! ${filledCount} designaç${
            filledCount > 1 ? "ões preenchidas" : "ão preenchida"
          }.`
        : "✅ Sugestões recalculadas — não havia nada em branco pra preencher."
    );
  };

  const setSlot = (entryId, partId, slot, personId) => {
    setEntries((prev) =>
      prev.map((e) =>
        e.id !== entryId
          ? e
          : {
              ...e,
              assignments: {
                ...e.assignments,
                [partId]: { ...e.assignments[partId], [slot]: personId || null },
              },
            }
      )
    );
    setMonthSaved(false);
  };

  const clearEntryAssignments = (entryId) => {
    setEntries((prev) =>
      prev.map((e) => {
        if (e.id !== entryId) return e;
        const cleared = {};
        for (const part of e.parts) {
          cleared[part.id] = {
            main: null,
            assistant: null,
            interpretar: (e.assignments[part.id] || {}).interpretar || false,
          };
        }
        return { ...e, assignments: cleared };
      })
    );
    setMonthSaved(false);
  };

  const toggleInterpretar = (entryId, partId) => {
    setEntries((prev) =>
      prev.map((e) =>
        e.id !== entryId
          ? e
          : {
              ...e,
              assignments: {
                ...e.assignments,
                [partId]: {
                  ...e.assignments[partId],
                  interpretar: !(e.assignments[partId] || {}).interpretar,
                  // Muda o modo de seleção — o nome escolhido no modo anterior
                  // pode não fazer sentido no novo, então limpa o designado.
                  main: null,
                },
              },
            }
      )
    );
    setMonthSaved(false);
  };

  const eligibleFor = (typeId) => {
    const onlyEfetivos = EFETIVO_ONLY_TYPES.has(typeId);
    return people
      .filter((p) => p.qualifications.includes(typeId) && (!onlyEfetivos || p.efetivo !== false))
      .sort((a, b) => a.name.localeCompare(b.name));
  };

  const nameOf = (id) => people.find((p) => p.id === id)?.name || "";
  const resolveName = (id) => {
    if (!id) return "—";
    const p = people.find((pp) => pp.id === id);
    return p ? p.name : "(removido)";
  };

  /* ---- Gerar PDF (via impressão do navegador) ---- */
  useEffect(() => {
    if (printData && printRequested.current) {
      printRequested.current = false;
      const t = setTimeout(() => window.print(), 150);
      return () => clearTimeout(t);
    }
  }, [printData]);

  const triggerPrint = (weeks, subtitle) => {
    setPrintSubtitle(subtitle || "");
    printRequested.current = true;
    setPrintData(weeks);
  };

  const printEntries = (list, subtitle) => {
    const weeks = list.map((e) => ({
      title: e.weekTitle || (e.mode === "sem_reuniao" ? "Sem reunião" : "Semana"),
      date: e.date,
      noMeeting: e.mode === "sem_reuniao",
      reason: e.skipReason,
      parts: e.parts,
      assignments: e.assignments,
    }));
    triggerPrint(weeks, subtitle);
  };

  // O PDF sai só do mês que está aberto, não do lote inteiro.
  const printOneMonth = (monthLabel) => {
    const monthEntries = entries.filter((e) => monthLabelOf(e.date) === monthLabel);
    printEntries(monthEntries, congName ? `${congName} — ${monthLabel}` : monthLabel);
  };

  const printOneWeek = (weekData) => triggerPrint([weekData], congName);

  const printAllHistory = async () => {
    const all = [];
    for (const w of history) {
      try {
        const r = await storage.get(`week:${w.id}`);
        if (r) all.push(JSON.parse(r.value));
      } catch {}
    }
    all.sort((a, b) => (a.date < b.date ? -1 : 1));
    triggerPrint(all, congName);
  };

  const readyToSave = entries.length > 0;
  const openEntry = entries.find((e) => e.id === openEntryId) || null;
  const openEntryIndex = openEntry ? entries.findIndex((e) => e.id === openEntryId) : -1;
  const monthGroups = groupEntriesByMonth(entries);
  const openMonthGroup = openMonthLabel ? monthGroups.find((g) => g.label === openMonthLabel) || null : null;

  /* ---- Salvar o mês inteiro ---- */
  const saveMonth = async () => {
    if (!readyToSave) return;
    setSaveError("");
    try {
      const newIndexItems = [];
      let updatedPeople = people.map((p) => ({
        ...p,
        lastAssigned: { ...(p.lastAssigned || {}) },
        lastPartner: { ...(p.lastPartner || {}) },
      }));
      let dayOffset = 0;
      const baseDate = entries.find((e) => e.date)?.date || todayISO();

      for (const entry of entries) {
        const id = uid();
        const date = entry.date || addDaysISO(baseDate, dayOffset);
        dayOffset += 7;

        if (entry.mode === "sem_reuniao") {
          const weekData = {
            id,
            date,
            title: entry.weekTitle || "Sem reunião",
            noMeeting: true,
            reason: entry.skipReason || "",
            parts: [],
            assignments: {},
          };
          await storage.set(`week:${id}`, JSON.stringify(weekData));
          newIndexItems.push({ id, date, title: weekData.title, noMeeting: true });
          continue;
        }

        const weekData = {
          id,
          date,
          title: entry.weekTitle || "Semana sem título",
          parts: entry.parts,
          assignments: entry.assignments,
        };
        await storage.set(`week:${id}`, JSON.stringify(weekData));
        newIndexItems.push({ id, date, title: weekData.title, noMeeting: false });

        for (const part of entry.parts) {
          const a = entry.assignments[part.id];
          if (!a) continue;
          updatedPeople = updatedPeople.map((p) => {
            if (a.main === p.id || a.assistant === p.id) {
              const nextPartner = { ...p.lastPartner };
              if (part.needsAssistant && a.main && a.assistant) {
                if (a.main === p.id) nextPartner[a.assistant] = date;
                else if (a.assistant === p.id) nextPartner[a.main] = date;
              }
              return {
                ...p,
                lastAssigned: { ...p.lastAssigned, [part.typeId]: date },
                lastPartner: nextPartner,
              };
            }
            return p;
          });
        }
      }

      const nextIndex = [...newIndexItems, ...history].sort((a, b) => (a.date < b.date ? 1 : -1));
      await storage.set("weeks-index", JSON.stringify(nextIndex));
      setHistory(nextIndex);
      await savePeople(updatedPeople);

      setMonthSaved(true);
    } catch (e) {
      setSaveError("Não consegui salvar o mês. Tente novamente.");
    }
  };

  const startNewMonth = () => {
    setEntries([]);
    setImportError("");
    setImportOkMsg("");
    setOpenEntryId(null);
    setOpenMonthLabel(null);
    setMonthSaved(false);
  };

  /* Recalcula do zero, a partir de TODO o histórico salvo, a última data em
     que cada pessoa fez cada tipo de parte. É isso que o rodízio usa pra
     decidir quem está "há mais tempo sem fazer" — se não recalcularmos
     depois de editar ou excluir uma semana já salva, uma alteração antiga
     nunca refletiria nas sugestões dos próximos meses. */
  const recomputeLastAssignedFromHistory = async (historyList) => {
    const weeks = [];
    for (const w of historyList) {
      if (w.noMeeting) continue;
      try {
        const r = await storage.get(`week:${w.id}`);
        if (r) weeks.push(JSON.parse(r.value));
      } catch {}
    }
    weeks.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));

    const lastByPerson = {};
    const partnerByPerson = {};
    for (const w of weeks) {
      if (!w.parts || !w.assignments) continue;
      for (const part of w.parts) {
        const a = w.assignments[part.id];
        if (!a) continue;
        for (const personId of [a.main, a.assistant]) {
          if (!personId) continue;
          if (!lastByPerson[personId]) lastByPerson[personId] = {};
          // Semanas em ordem cronológica — quem vier depois sobrescreve,
          // então no final sobra sempre a ocorrência mais recente.
          lastByPerson[personId][part.typeId] = w.date;
        }
        if (part.needsAssistant && a.main && a.assistant) {
          if (!partnerByPerson[a.main]) partnerByPerson[a.main] = {};
          if (!partnerByPerson[a.assistant]) partnerByPerson[a.assistant] = {};
          partnerByPerson[a.main][a.assistant] = w.date;
          partnerByPerson[a.assistant][a.main] = w.date;
        }
      }
    }

    await savePeople(
      people.map((p) => ({
        ...p,
        lastAssigned: lastByPerson[p.id] || {},
        lastPartner: partnerByPerson[p.id] || {},
      }))
    );
  };

  const handleRecomputeRotation = async () => {
    setRotationBusy(true);
    setRotationMsg("");
    try {
      await recomputeLastAssignedFromHistory(history);
      setRotationMsg("Rodízio recalculado a partir do histórico salvo.");
    } catch {
      setRotationMsg("Não consegui recalcular. Tente de novo.");
    } finally {
      setRotationBusy(false);
    }
  };

  /* ---- Histórico ---- */
  const openWeek = async (id) => {
    setWeekEditing(false);
    setWeekSaveMsg("");
    try {
      const r = await storage.get(`week:${id}`);
      const week = r ? JSON.parse(r.value) : null;
      setViewingWeek(week ? { ...week, parts: fixPartsTypeIds(week.parts) } : null);
    } catch {
      setViewingWeek(null);
    }
  };
  const deleteWeek = async (id) => {
    try {
      await storage.delete(`week:${id}`);
    } catch {}
    const nextIndex = history.filter((w) => w.id !== id);
    await storage.set("weeks-index", JSON.stringify(nextIndex));
    setHistory(nextIndex);
    if (viewingWeek?.id === id) setViewingWeek(null);
    await recomputeLastAssignedFromHistory(nextIndex);
  };

  /* Editar uma semana já salva no histórico */
  const updateViewingWeek = (patch) => setViewingWeek((v) => (v ? { ...v, ...patch } : v));
  const setViewingWeekSlot = (partId, slot, personId) =>
    setViewingWeek((v) =>
      v
        ? {
            ...v,
            assignments: {
              ...v.assignments,
              [partId]: { ...v.assignments[partId], [slot]: personId || null },
            },
          }
        : v
    );

  const saveWeekEdits = async () => {
    if (!viewingWeek) return;
    setWeekSaveMsg("");
    try {
      await storage.set(`week:${viewingWeek.id}`, JSON.stringify(viewingWeek));
      const nextIndex = history.map((w) =>
        w.id === viewingWeek.id ? { ...w, title: viewingWeek.title, date: viewingWeek.date } : w
      );
      await storage.set("weeks-index", JSON.stringify(nextIndex));
      setHistory(nextIndex);
      await recomputeLastAssignedFromHistory(nextIndex);
      setWeekEditing(false);
      setWeekSaveMsg("Alterações salvas — o rodízio dos próximos meses já leva essa mudança em conta.");
    } catch {
      setWeekSaveMsg("Não consegui salvar as alterações. Tente de novo.");
    }
  };

  const groupParts = (parts) => {
    const grouped = [];
    let currentSection = null;
    for (const part of parts) {
      const sec = sectionOf(part.typeId);
      if (sec !== "geral" && sec !== currentSection) {
        grouped.push({ kind: "header", section: sec });
        currentSection = sec;
      }
      if (sec === "geral") currentSection = null;
      grouped.push({ kind: "part", part });
    }
    return grouped;
  };

  if (!localConfig.syncSecret) {
    return (
      <div className="dapp">
        <style>{CSS}</style>
        <LoginScreen onLogin={handleLogin} />
      </div>
    );
  }

  return (
    <div className="dapp">
      <style>{CSS}</style>

      <header className="dapp-header">
        <div className="dapp-eyebrow">Designações da reunião</div>
        {congName && <div className="dapp-cong-name">{congName}</div>}
      </header>

      <nav className="dapp-tabs">
        {[
          ["semanas", "Programação"],
          ["pessoas", "Pessoas"],
          ["historico", "Histórico"],
          ["ajustes", "Ajustes"],
        ].map(([id, label]) => (
          <button
            key={id}
            className={"dapp-tab" + (tab === id ? " is-active" : "")}
            onClick={() => setTab(id)}
          >
            {label}
          </button>
        ))}
      </nav>

      <main className="dapp-main">
        {/* ---------------- ABA PROGRAMAÇÃO ---------------- */}
        {tab === "semanas" && (
          <section>
            {openEntry ? (
              <div>
                <button className="dapp-btn" onClick={() => setOpenEntryId(null)}>
                  ← Voltar {openMonthGroup ? `a ${openMonthGroup.label}` : "à lista"}
                </button>
                <WeekEntryCard
                  index={openEntryIndex}
                  entry={openEntry}
                  onChangeDate={(v) => updateEntry(openEntry.id, { date: v })}
                  onChangeSkipReason={(v) => updateEntry(openEntry.id, { skipReason: v })}
                  onChangeTitle={(v) => updateEntry(openEntry.id, { weekTitle: v })}
                  onToggleSemReuniao={() => toggleSemReuniao(openEntry.id)}
                  onRemove={() =>
                    askConfirm("Remover esta semana da lista?", () => removeEntry(openEntry.id))
                  }
                  eligibleFor={eligibleFor}
                  people={people}
                  nameOf={nameOf}
                  groupParts={groupParts}
                  onChangeMain={(partId, pid) => setSlot(openEntry.id, partId, "main", pid)}
                  onChangeAssistant={(partId, pid) => setSlot(openEntry.id, partId, "assistant", pid)}
                  onToggleInterpretar={(partId) => toggleInterpretar(openEntry.id, partId)}
                  onClearAssignments={() =>
                    askConfirm(
                      "Limpar todas as designações desta semana? As partes continuam, só os nomes saem.",
                      () => clearEntryAssignments(openEntry.id),
                      "Limpar"
                    )
                  }
                />
              </div>
            ) : openMonthGroup ? (
              <div>
                <button
                  className="dapp-btn"
                  onClick={() => {
                    setOpenMonthLabel(null);
                    setRecalcMsg("");
                  }}
                >
                  ← Voltar aos meses
                </button>
                <h3 className="dapp-week-title">{openMonthGroup.label}</h3>

                <div className="dapp-card">
                  {openMonthGroup.entries.map((e) => (
                    <div key={e.id} className="dapp-week-row" onClick={() => setOpenEntryId(e.id)}>
                      <div>
                        <div className="dapp-week-row-title">
                          {e.weekTitle || "Semana"}
                          {e.mode === "sem_reuniao" && <span className="dapp-tag-skip">evento</span>}
                        </div>
                        <div className="dapp-hint">{e.date || "sem data"}</div>
                      </div>
                      <span className="dapp-chevron">›</span>
                    </div>
                  ))}
                </div>

                <div className="dapp-actions-row" style={{ marginTop: 10 }}>
                  <button className="dapp-btn" onClick={() => recalcMonthSuggestions(openMonthGroup.label)}>
                    Recalcular sugestões deste mês
                  </button>
                </div>
                {recalcMsg && (
                  <p className="dapp-hint" style={{ marginTop: 4, fontWeight: 600 }}>
                    {recalcMsg}
                  </p>
                )}
                <p className="dapp-hint" style={{ marginTop: 6 }}>
                  Só preenche o que estiver em branco — designações já feitas não mudam. Pra
                  recalcular uma parte específica, deixe-a em "— Selecionar —" antes.
                </p>

                <button
                  className="dapp-btn dapp-btn-block"
                  onClick={() => printOneMonth(openMonthGroup.label)}
                >
                  Gerar PDF deste mês
                </button>
              </div>
            ) : (
              <>
                <p className="dapp-hint dapp-intro">
                  Baixe a programação do bimestre em PDF no site oficial (jw.org) e importe aqui.
                  Pode importar mais de um arquivo, um de cada vez — cada um soma às semanas já
                  importadas (uma semana com a mesma data de outra já importada é substituída).
                </p>

                {!proxyUrl && (
                  <p className="dapp-error">
                    Servidor não configurado. Defina o endereço na aba <b>Ajustes</b> para
                    conseguir importar o PDF.
                  </p>
                )}

                <div className="dapp-card">
                  <button
                    className="dapp-btn dapp-btn-primary dapp-btn-block"
                    onClick={() => bimesterInputRef.current?.click()}
                    disabled={importing}
                  >
                    {importing ? "Lendo o PDF…" : "Importar PDF do bimestre"}
                  </button>
                  <input
                    ref={bimesterInputRef}
                    type="file"
                    accept="application/pdf"
                    style={{ display: "none" }}
                    onChange={(e) => {
                      importBimesterPdf(e.target.files?.[0]);
                      e.target.value = "";
                    }}
                  />
                  {importError && <p className="dapp-error">{importError}</p>}
                  {importOkMsg && !importing && (
                    <p className="dapp-hint" style={{ marginTop: 6, fontWeight: 600 }}>
                      {importOkMsg}
                    </p>
                  )}
                </div>

                {people.length === 0 && peopleLoaded && (
                  <p className="dapp-hint">
                    Dica: cadastre as pessoas e as partes que elas podem fazer na aba{" "}
                    <b>Pessoas</b> antes de importar o PDF.
                  </p>
                )}

                {monthGroups.map((g) => (
                  <div key={g.label} className="dapp-card">
                    <div
                      className="dapp-week-row"
                      style={{ borderBottom: "none", padding: 0 }}
                      onClick={() => {
                        setOpenMonthLabel(g.label);
                        setRecalcMsg("");
                      }}
                    >
                      <div>
                        <div className="dapp-week-row-title">{g.label}</div>
                        <div className="dapp-hint">
                          {g.entries.length} semana{g.entries.length > 1 ? "s" : ""}
                        </div>
                      </div>
                      <span className="dapp-chevron">›</span>
                    </div>
                  </div>
                ))}

                <button className="dapp-btn dapp-btn-block" onClick={addWeekEntry}>
                  + Adicionar semana avulsa (evento)
                </button>

                {saveError && <p className="dapp-error">{saveError}</p>}

                <button
                  className="dapp-btn dapp-btn-primary dapp-btn-block"
                  onClick={saveMonth}
                  disabled={!readyToSave}
                >
                  {monthSaved ? "Mês salvo ✓" : "Confirmar e salvar o mês"}
                </button>

                {monthSaved && (
                  <>
                    <button
                      className="dapp-btn dapp-btn-block"
                      onClick={() =>
                        askConfirm(
                          "Começar outro mês? Isso limpa as semanas atuais desta tela (o que já foi salvo no histórico não é afetado).",
                          startNewMonth,
                          "Começar outro mês"
                        )
                      }
                    >
                      Começar outro mês
                    </button>
                  </>
                )}
              </>
            )}
          </section>
        )}

        {/* ---------------- ABA PESSOAS ---------------- */}
        {tab === "pessoas" && (
          <section>
            <div className="dapp-card">
              <label className="dapp-label">Adicionar pessoa</label>
              <div className="dapp-add-row">
                <input
                  className="dapp-input"
                  placeholder="Nome"
                  value={newName}
                  onChange={(e) => setNewName(e.target.value)}
                  onKeyDown={(e) => e.key === "Enter" && addPerson()}
                />
                <div className="dapp-gender-switch">
                  {[
                    ["M", "Masc."],
                    ["F", "Fem."],
                  ].map(([g, label]) => (
                    <button
                      key={g}
                      type="button"
                      className={"dapp-gender-btn" + (newGender === g ? " is-active" : "")}
                      onClick={() => setNewGender(g)}
                    >
                      {label}
                    </button>
                  ))}
                </div>
                <button className="dapp-btn dapp-btn-primary" onClick={addPerson}>
                  Adicionar
                </button>
              </div>
            </div>

            {!peopleLoaded ? (
              <p className="dapp-hint">Carregando…</p>
            ) : people.length === 0 ? (
              <p className="dapp-hint">
                Nenhuma pessoa cadastrada ainda. Adicione os irmãos e irmãs habilitados acima.
              </p>
            ) : (
              people
                .slice()
                .sort((a, b) => a.name.localeCompare(b.name))
                .map((p) => (
                  <PersonCard
                    key={p.id}
                    person={p}
                    onToggle={(typeId) => toggleQualification(p.id, typeId)}
                    onToggleEfetivo={() => toggleEfetivo(p.id)}
                    onToggleRotationExempt={() => toggleRotationExempt(p.id)}
                    onSetGender={(gender) => setGender(p.id, gender)}
                    onRename={(name) => renamePerson(p.id, name)}
                    onRemove={() =>
                      askConfirm(`Remover ${p.name} da lista de pessoas?`, () => removePerson(p.id))
                    }
                  />
                ))
            )}
          </section>
        )}

        {/* ---------------- ABA HISTÓRICO ---------------- */}
        {tab === "historico" && (
          <section>
            {!historyLoaded ? (
              <p className="dapp-hint">Carregando…</p>
            ) : history.length === 0 ? (
              <p className="dapp-hint">Nenhuma semana salva ainda.</p>
            ) : viewingWeek ? (
              <div>
                <button className="dapp-btn" onClick={() => setViewingWeek(null)}>
                  ← Voltar
                </button>

                {weekEditing ? (
                  <>
                    <label className="dapp-label" style={{ marginTop: 10 }}>
                      Título da semana
                    </label>
                    <input
                      className="dapp-input dapp-week-title-input"
                      value={viewingWeek.title}
                      onChange={(e) => updateViewingWeek({ title: e.target.value })}
                    />
                    <label className="dapp-label" style={{ marginTop: 10 }}>
                      Data
                    </label>
                    <input
                      className="dapp-input"
                      type="date"
                      value={viewingWeek.date || ""}
                      onChange={(e) => updateViewingWeek({ date: e.target.value })}
                    />
                    {viewingWeek.noMeeting && (
                      <>
                        <label className="dapp-label" style={{ marginTop: 10 }}>
                          Motivo
                        </label>
                        <input
                          className="dapp-input"
                          value={viewingWeek.reason || ""}
                          onChange={(e) => updateViewingWeek({ reason: e.target.value })}
                        />
                      </>
                    )}
                  </>
                ) : (
                  <>
                    <h3 className="dapp-week-title">{viewingWeek.title}</h3>
                    <p className="dapp-hint">{viewingWeek.date}</p>
                  </>
                )}

                {viewingWeek.noMeeting ? (
                  !weekEditing && (
                    <p className="dapp-hint" style={{ marginTop: 8 }}>
                      Sem reunião{viewingWeek.reason ? ` — ${viewingWeek.reason}` : ""}.
                    </p>
                  )
                ) : weekEditing ? (
                  <div style={{ marginTop: 10 }}>
                    {groupParts(viewingWeek.parts).map((item, i) =>
                      item.kind === "header" ? (
                        <div
                          key={"h" + i}
                          className="dapp-section-bar"
                          style={{ background: SECTIONS[item.section].color }}
                        >
                          {SECTIONS[item.section].label}
                        </div>
                      ) : (
                        <PartRow
                          key={item.part.id}
                          part={item.part}
                          assignment={viewingWeek.assignments[item.part.id] || {}}
                          eligibleFor={eligibleFor}
                          onChangeMain={(pid) => setViewingWeekSlot(item.part.id, "main", pid)}
                          onChangeAssistant={(pid) => setViewingWeekSlot(item.part.id, "assistant", pid)}
                        />
                      )
                    )}
                  </div>
                ) : (
                  viewingWeek.parts.map((part) => {
                    const a = viewingWeek.assignments[part.id] || {};
                    return (
                      <div key={part.id} className="dapp-history-row">
                        <div className="dapp-history-title">{part.title}</div>
                        <div className="dapp-history-names">
                          {resolveName(a.main)}
                          {a.interpretar ? " (Interpretar)" : ""}
                          {part.needsAssistant ? ` / ${resolveName(a.assistant)}` : ""}
                        </div>
                      </div>
                    );
                  })
                )}

                {weekSaveMsg && (
                  <p className="dapp-hint" style={{ marginTop: 8 }}>
                    {weekSaveMsg}
                  </p>
                )}

                {weekEditing ? (
                  <div className="dapp-actions-row" style={{ marginTop: 12 }}>
                    <button className="dapp-btn dapp-btn-primary" onClick={saveWeekEdits}>
                      Salvar alterações
                    </button>
                    <button className="dapp-btn" onClick={() => openWeek(viewingWeek.id)}>
                      Cancelar
                    </button>
                  </div>
                ) : (
                  <>
                    <button
                      className="dapp-btn dapp-btn-block"
                      onClick={() => setWeekEditing(true)}
                      style={{ marginTop: 12 }}
                    >
                      Editar esta semana
                    </button>
                    <button className="dapp-btn dapp-btn-block" onClick={() => printOneWeek(viewingWeek)}>
                      Gerar PDF desta semana
                    </button>
                  </>
                )}
              </div>
            ) : (
              <>
                <button className="dapp-btn dapp-btn-block" onClick={printAllHistory}>
                  Gerar PDF de todas as semanas salvas
                </button>
                {history.map((w) => (
                  <div key={w.id} className="dapp-card dapp-history-card">
                    <div onClick={() => openWeek(w.id)} className="dapp-history-click">
                      <div className="dapp-week-title">
                        {w.title}
                        {w.noMeeting ? <span className="dapp-tag-skip">sem reunião</span> : null}
                      </div>
                      <div className="dapp-hint">{w.date}</div>
                    </div>
                    <button
                      className="dapp-btn-text dapp-btn-danger"
                      onClick={() =>
                        askConfirm(
                          "Excluir esta semana do histórico? Essa ação não pode ser desfeita.",
                          () => deleteWeek(w.id)
                        )
                      }
                    >
                      Excluir
                    </button>
                  </div>
                ))}
              </>
            )}
          </section>
        )}

        {/* ---------------- ABA AJUSTES ---------------- */}
        {tab === "ajustes" && (
          <section>
            <div className="dapp-card">
              <label className="dapp-label">Nome da congregação</label>
              <input
                className="dapp-input"
                placeholder="Nome da congregação"
                value={congName}
                onChange={(e) => saveCloudConfig({ congName: e.target.value })}
              />
            </div>

            <div className="dapp-card">
              <label className="dapp-label">Alertas</label>
              <label className="dapp-checkbox-row" style={{ marginBottom: cloudConfig.reminderEnabled ? 12 : 0 }}>
                <input
                  type="checkbox"
                  checked={!!cloudConfig.reminderEnabled}
                  onChange={(e) => handleReminderToggle(e.target.checked)}
                  disabled={pushBusy}
                />
                <span>Avisar quando a programação do próximo mês não estiver pronta</span>
              </label>

              {cloudConfig.reminderEnabled && (
                <>
                  <label className="dapp-label">Avisar com quantos dias de antecedência</label>
                  <input
                    className="dapp-input"
                    type="number"
                    min={1}
                    max={60}
                    value={cloudConfig.reminderDays ?? 7}
                    onChange={(e) =>
                      saveCloudConfig({ reminderDays: Math.max(1, parseInt(e.target.value, 10) || 7) })
                    }
                    style={{ maxWidth: 110 }}
                  />
                  <button
                    className="dapp-btn"
                    style={{ marginTop: 10 }}
                    onClick={sendTestPush}
                    disabled={pushBusy}
                  >
                    Enviar notificação de teste
                  </button>
                </>
              )}

              {pushStatus && (
                <p className="dapp-hint" style={{ marginTop: 10 }}>
                  {pushStatus}
                </p>
              )}
              <p className="dapp-hint" style={{ marginTop: 10 }}>
                Funciona mesmo com o app fechado, mas o navegador precisa autorizar
                notificações. No iPhone só funciona depois de instalar o app na tela de início
                (Compartilhar → Adicionar à Tela de Início) — pelo Safari comum não funciona.
              </p>
            </div>

            <div className="dapp-card">
              <label className="dapp-label">Conta</label>
              <p className="dapp-hint" style={{ margin: "0 0 10px" }}>
                Você está conectado com a senha deste aparelho.
              </p>
              <button
                className="dapp-btn dapp-btn-block"
                onClick={() => askConfirm("Sair da conta neste aparelho?", handleLogout, "Sair")}
              >
                Sair
              </button>
            </div>

            <div className="dapp-card">
              <label className="dapp-label">Endereço do servidor (avançado)</label>
              <input
                className="dapp-input"
                placeholder="https://designacoes-proxy.SEU-USUARIO.workers.dev"
                value={connectDraft.proxyUrl}
                onChange={(e) => setConnectDraft((d) => ({ ...d, proxyUrl: e.target.value }))}
                autoComplete="off"
                autoCapitalize="off"
                spellCheck={false}
              />
              <button
                className="dapp-btn dapp-btn-block"
                style={{ marginTop: 10 }}
                onClick={() => connectCloud({ ...connectDraft, syncSecret: localConfig.syncSecret })}
              >
                Salvar servidor
              </button>
              {syncStatus && (
                <p className="dapp-hint" style={{ marginTop: 8 }}>
                  {syncStatus}
                </p>
              )}
              <p className="dapp-hint" style={{ marginTop: 8 }}>
                Só mexa aqui se você migrou pra outro servidor — o app já vem configurado com o
                endereço certo. Veja o arquivo README.md do projeto se for publicar o seu próprio.
              </p>
            </div>

            <div className="dapp-card">
              <label className="dapp-label">Rodízio</label>
              <p className="dapp-hint" style={{ margin: "0 0 8px" }}>
                Editar ou excluir uma semana no Histórico já atualiza automaticamente quem fez o
                quê por último. Se desconfiar que o rodízio ficou desalinhado com o histórico
                (por exemplo, de antes dessa correção), recalcule aqui — não mexe nas semanas
                salvas, só ajusta de quem é a vez.
              </p>
              <button className="dapp-btn dapp-btn-block" onClick={handleRecomputeRotation} disabled={rotationBusy}>
                {rotationBusy ? "Recalculando…" : "Recalcular rodízio a partir do histórico"}
              </button>
              {rotationMsg && (
                <p className="dapp-hint" style={{ marginTop: 8 }}>
                  {rotationMsg}
                </p>
              )}
            </div>

            <div className="dapp-card">
              <label className="dapp-label">Backup dos dados</label>
              <p className="dapp-hint" style={{ margin: "0 0 8px" }}>
                Além da nuvem, você pode baixar uma cópia de segurança de vez em quando e guardar
                o arquivo em local seguro.
              </p>
              <div className="dapp-actions-row" style={{ marginBottom: 0 }}>
                <button className="dapp-btn" onClick={exportBackup}>
                  Exportar backup
                </button>
                <button className="dapp-btn" onClick={() => backupInputRef.current?.click()}>
                  Importar backup
                </button>
              </div>
              <input
                ref={backupInputRef}
                type="file"
                accept="application/json,.json"
                style={{ display: "none" }}
                onChange={(e) => {
                  importBackup(e.target.files?.[0]);
                  e.target.value = "";
                }}
              />
              {importMsg && (
                <p className="dapp-hint" style={{ marginTop: 8 }}>
                  {importMsg}
                </p>
              )}
            </div>
          </section>
        )}
      </main>

      {confirmDialog && (
        <div className="dapp-modal-backdrop" onClick={() => setConfirmDialog(null)}>
          <div className="dapp-modal" onClick={(e) => e.stopPropagation()}>
            <div className="dapp-modal-title">⚠️ Confirmação</div>
            <p className="dapp-modal-msg">{confirmDialog.message}</p>
            <div className="dapp-modal-actions">
              <button className="dapp-btn" onClick={() => setConfirmDialog(null)}>
                Cancelar
              </button>
              <button
                className="dapp-btn dapp-btn-danger-solid"
                onClick={() => {
                  const fn = confirmDialog.onConfirm;
                  setConfirmDialog(null);
                  fn();
                }}
              >
                {confirmDialog.confirmLabel}
              </button>
            </div>
          </div>
        </div>
      )}

      {/* Área usada apenas para gerar o PDF (impressão do navegador) */}
      {printData && (
        <div className="dapp-print-area">
          <h1 className="print-doc-title">Programação de designações</h1>
          {printSubtitle && <p className="print-doc-subtitle">{printSubtitle}</p>}
          {printData.map((w, wi) => (
            <div key={wi} className="print-week">
              <h2 className="print-week-title">
                {w.title}
                {w.date ? ` — ${w.date}` : ""}
              </h2>
              {w.noMeeting ? (
                <div className="print-event-box">
                  <div className="print-event-label">EVENTO</div>
                  <div className="print-event-name">{w.reason || "Sem reunião"}</div>
                </div>
              ) : (
                groupParts(w.parts).map((item, i) =>
                  item.kind === "header" ? (
                    <div
                      key={"h" + i}
                      className="print-section-bar"
                      style={{ background: SECTIONS[item.section].color }}
                    >
                      {SECTIONS[item.section].label}
                    </div>
                  ) : (
                    <div key={item.part.id} className="print-part-row">
                      <span className="print-part-title">
                        {item.part.number != null ? `${item.part.number}. ` : ""}
                        {item.part.title}
                        {item.part.minutes ? ` (${item.part.minutes} min)` : ""}
                      </span>
                      <span className="print-part-names">
                        {resolveName((w.assignments[item.part.id] || {}).main)}
                        {(w.assignments[item.part.id] || {}).interpretar ? " (Interpretar)" : ""}
                        {item.part.needsAssistant
                          ? ` / ${resolveName((w.assignments[item.part.id] || {}).assistant)}`
                          : ""}
                      </span>
                    </div>
                  )
                )
              )}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

/* ---------------------------------------------------------
   Tela de login (senha única compartilhada)
--------------------------------------------------------- */
function LoginScreen({ onLogin }) {
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");
  const [loading, setLoading] = useState(false);

  const submit = async (e) => {
    e.preventDefault();
    if (!password.trim() || loading) return;
    setLoading(true);
    setError("");
    try {
      await onLogin(password.trim());
    } catch (err) {
      setError(
        err && err.message === "SEM_SERVIDOR"
          ? "Servidor não configurado neste aparelho."
          : "Senha incorreta, ou não consegui falar com o servidor."
      );
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="dapp-login">
      <div className="dapp-eyebrow">Designações da reunião</div>
      <h1 className="dapp-login-title">Entrar</h1>
      <form onSubmit={submit}>
        <label className="dapp-label">Senha</label>
        <input
          className="dapp-input"
          type="password"
          autoFocus
          autoComplete="current-password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
        {error && <p className="dapp-error">{error}</p>}
        <button
          className="dapp-btn dapp-btn-primary dapp-btn-block"
          type="submit"
          disabled={loading || !password.trim()}
          style={{ marginTop: 12 }}
        >
          {loading ? "Entrando…" : "Entrar"}
        </button>
      </form>
    </div>
  );
}

/* ---------------------------------------------------------
   Cartão de uma semana no planejamento do mês
--------------------------------------------------------- */
function WeekEntryCard({
  index,
  entry,
  onChangeDate,
  onChangeSkipReason,
  onChangeTitle,
  onToggleSemReuniao,
  onRemove,
  eligibleFor,
  people,
  nameOf,
  groupParts,
  onChangeMain,
  onChangeAssistant,
  onToggleInterpretar,
  onClearAssignments,
}) {
  const isEvent = entry.mode === "sem_reuniao";

  return (
    <div className="dapp-card dapp-entry">
      <div className="dapp-entry-head">
        <span className="dapp-entry-num">
          Semana {index + 1}
          {isEvent && <span className="dapp-tag-skip">evento</span>}
        </span>
        <button className="dapp-btn-text dapp-btn-danger" onClick={onRemove}>
          Remover
        </button>
      </div>

      <label className="dapp-checkbox-row" style={{ marginTop: 8 }}>
        <input type="checkbox" checked={isEvent} onChange={onToggleSemReuniao} />
        <span>
          Evento (congresso, assembleia, Celebração, visita do superintendente…) — sem reunião
        </span>
      </label>
      {isEvent && (
        <input
          className="dapp-input"
          placeholder="Nome do evento — ex.: Assembleia de Circuito"
          value={entry.skipReason}
          onChange={(e) => onChangeSkipReason(e.target.value)}
          style={{ marginTop: 6 }}
        />
      )}

      <label className="dapp-label" style={{ marginTop: 10 }}>
        Data da semana (opcional)
      </label>
      <input
        className="dapp-input"
        type="date"
        value={entry.date}
        onChange={(e) => onChangeDate(e.target.value)}
      />

      {isEvent ? (
        <p className="dapp-hint" style={{ marginTop: 10 }}>
          Semana marcada como evento — sem designações.
        </p>
      ) : (
        <div style={{ marginTop: 10 }}>
          <input
            className="dapp-input dapp-week-title-input"
            value={entry.weekTitle}
            onChange={(e) => onChangeTitle(e.target.value)}
          />
          {entry.parts.length > 0 && (
            <button
              className="dapp-btn-text dapp-btn-danger"
              onClick={onClearAssignments}
              style={{ marginBottom: 6 }}
            >
              Limpar designações desta semana
            </button>
          )}
          {entry.parts.length === 0 ? (
            <p className="dapp-hint">Nenhuma parte nesta semana.</p>
          ) : (
            groupParts(entry.parts).map((item, i) =>
              item.kind === "header" ? (
                <div
                  key={"h" + i}
                  className="dapp-section-bar"
                  style={{ background: SECTIONS[item.section].color }}
                >
                  {SECTIONS[item.section].label}
                </div>
              ) : (
                <PartRow
                  key={item.part.id}
                  part={item.part}
                  assignment={entry.assignments[item.part.id] || {}}
                  eligibleFor={eligibleFor}
                  people={people}
                  onChangeMain={(pid) => onChangeMain(item.part.id, pid)}
                  onChangeAssistant={(pid) => onChangeAssistant(item.part.id, pid)}
                  onToggleInterpretar={() => onToggleInterpretar(item.part.id)}
                />
              )
            )
          )}
        </div>
      )}
    </div>
  );
}

/* ---------------------------------------------------------
   Linha de uma parte
--------------------------------------------------------- */
function PartRow({ part, assignment, eligibleFor, people, onChangeMain, onChangeAssistant, onToggleInterpretar }) {
  const sec = sectionOf(part.typeId);
  const color = SECTIONS[sec].color;
  const canInterpretar = INTERPRETAR_TYPE_IDS.has(part.typeId);
  const isInterpretar = canInterpretar && !!assignment.interpretar;

  const mainOptions = isInterpretar
    ? (people || []).filter((p) => p.gender === "M").slice().sort((a, b) => a.name.localeCompare(b.name))
    : eligibleFor(part.typeId);
  const mainPerson = mainOptions.find((p) => p.id === assignment.main);
  const assistantOptions = eligibleFor(part.typeId).filter(
    (p) =>
      p.id !== assignment.main &&
      // No Ministério, ajudante tem que ser do mesmo sexo do estudante...
      (!mainPerson || !mainPerson.gender || !p.gender || p.gender === mainPerson.gender) &&
      // ...e dois ocasionais nunca formam dupla.
      !(mainPerson && mainPerson.efetivo === false && p.efetivo === false)
  );

  return (
    <div className="dapp-card dapp-part" style={{ borderLeft: `4px solid ${color}` }}>
      <div className="dapp-part-title">
        {part.number != null ? `${part.number}. ` : ""}
        {part.title}
        {part.minutes ? <span className="dapp-part-min"> ({part.minutes} min)</span> : null}
      </div>

      {canInterpretar && (
        <label className="dapp-checkbox-row" style={{ marginBottom: 8 }}>
          <input type="checkbox" checked={isInterpretar} onChange={onToggleInterpretar} />
          <span>Interpretar</span>
        </label>
      )}

      <div className="dapp-slot">
        <span className="dapp-slot-label">{part.needsAssistant ? "Estudante" : "Designado"}</span>
        <select className="dapp-select" value={assignment.main || ""} onChange={(e) => onChangeMain(e.target.value)}>
          <option value="">— Selecionar —</option>
          {mainOptions.map((p) => (
            <option key={p.id} value={p.id}>
              {p.name}
            </option>
          ))}
        </select>
        {mainOptions.length === 0 && (
          <p className="dapp-warn">
            {isInterpretar ? "Nenhum irmão cadastrado com sexo masculino." : "Ninguém cadastrado para esta parte."}
          </p>
        )}
      </div>

      {part.needsAssistant && (
        <div className="dapp-slot">
          <span className="dapp-slot-label">Ajudante</span>
          <select className="dapp-select" value={assignment.assistant || ""} onChange={(e) => onChangeAssistant(e.target.value)}>
            <option value="">— Selecionar —</option>
            {assistantOptions.map((p) => (
              <option key={p.id} value={p.id}>
                {p.name}
              </option>
            ))}
          </select>
        </div>
      )}
    </div>
  );
}

/* ---------------------------------------------------------
   Cartão de pessoa na aba Pessoas
--------------------------------------------------------- */
function PersonCard({
  person,
  onToggle,
  onToggleEfetivo,
  onToggleRotationExempt,
  onSetGender,
  onRename,
  onRemove,
}) {
  const [open, setOpen] = useState(false);
  const [nameDraft, setNameDraft] = useState(person.name);
  const count = person.qualifications.length;
  const isRegular = person.efetivo !== false;

  useEffect(() => {
    setNameDraft(person.name);
  }, [person.name]);

  const commitName = () => {
    const v = nameDraft.trim();
    if (v && v !== person.name) onRename(v);
    else setNameDraft(person.name);
  };

  return (
    <div className="dapp-card">
      <div className="dapp-person-head" onClick={() => setOpen(!open)}>
        <div>
          <div className="dapp-person-name">
            {person.name}
            {!isRegular && <span className="dapp-tag-skip">ocasional</span>}
            {!person.gender && <span className="dapp-tag-skip">sem sexo definido</span>}
            {person.rotationExempt && <span className="dapp-tag-skip">fora do rodízio</span>}
          </div>
          <div className="dapp-hint">
            {count === 0 ? "Nenhuma parte habilitada" : `${count} parte(s) habilitada(s)`}
          </div>
        </div>
        <span className="dapp-chevron">{open ? "▾" : "▸"}</span>
      </div>

      {open && (
        <div className="dapp-quals">
          <label className="dapp-label">Nome</label>
          <input
            className="dapp-input"
            value={nameDraft}
            onChange={(e) => setNameDraft(e.target.value)}
            onBlur={commitName}
            onKeyDown={(e) => e.key === "Enter" && e.target.blur()}
            style={{ marginBottom: 14 }}
          />

          <label className="dapp-label">Sexo</label>
          <div className="dapp-gender-switch" style={{ marginBottom: 12 }}>
            {[
              ["M", "Masculino"],
              ["F", "Feminino"],
            ].map(([g, label]) => (
              <button
                key={g}
                type="button"
                className={"dapp-gender-btn" + (person.gender === g ? " is-active" : "")}
                onClick={() => onSetGender(g)}
              >
                {label}
              </button>
            ))}
          </div>

          <label className="dapp-checkbox-row dapp-efetivo-row">
            <input type="checkbox" checked={isRegular} onChange={onToggleEfetivo} />
            <span>
              Membro efetivo do grupo
              <span className="dapp-hint dapp-efetivo-hint">
                {" "}
                — desmarque para quem recebe designações só esporadicamente (ex.: quem não é do
                grupo, como visitantes da língua de sinais). Recebe ~3x menos designações no rodízio.
              </span>
            </span>
          </label>

          <label className="dapp-checkbox-row dapp-efetivo-row">
            <input
              type="checkbox"
              checked={!!person.rotationExempt}
              onChange={onToggleRotationExempt}
            />
            <span>
              Não incluir no rodízio automático
              <span className="dapp-hint dapp-efetivo-hint">
                {" "}
                — pra opções que não são pessoas de verdade, tipo "Stream"/gravação usada às
                vezes no lugar de uma parte ao vivo. Continua aparecendo pra você escolher
                manualmente, só não entra nas sugestões automáticas.
              </span>
            </span>
          </label>

          {["tesouros", "ministerio", "vida", "geral"].map((sec) => (
            <div key={sec}>
              {SECTIONS[sec].label && (
                <div className="dapp-quals-section" style={{ color: SECTIONS[sec].color }}>
                  {SECTIONS[sec].label}
                </div>
              )}
              {PART_TYPES.filter((t) => (t.section || "geral") === sec).map((t) => (
                <label key={t.id} className="dapp-checkbox-row">
                  <input
                    type="checkbox"
                    checked={person.qualifications.includes(t.id)}
                    onChange={() => onToggle(t.id)}
                  />
                  {t.label}
                </label>
              ))}
            </div>
          ))}
          <button className="dapp-btn-text dapp-btn-danger dapp-remove" onClick={onRemove}>
            Remover pessoa
          </button>
        </div>
      )}
    </div>
  );
}

/* ---------------------------------------------------------
   Estilos
--------------------------------------------------------- */
const CSS = `
.dapp {
  --paper: #FFFFFF;
  --ink: #1F2328;
  --ink-soft: #6B7280;
  --line: #E5E3DD;
  --card: #FFFFFF;
  --accent: #4A6DA7;
  font-family: 'Work Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  background: var(--paper);
  color: var(--ink);
  max-width: 480px;
  margin: 0 auto;
  min-height: 100%;
  padding-bottom: 32px;
}
/* Telas largas (tablet/computador): vira um cartão centralizado com respiro
   em volta. Abaixo de 700px (celular) o app continua ocupando a tela toda,
   sem nenhuma mudança visual. */
@media (min-width: 700px) {
  .dapp {
    max-width: 640px;
    min-height: auto;
    margin: 36px auto;
    border-radius: 16px;
    box-shadow: 0 16px 48px rgba(31, 35, 40, 0.10);
    overflow: hidden;
  }
}
.dapp-header { padding: 22px 20px 14px; }
.dapp-eyebrow {
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: 6px;
}
.dapp-cong-name { font-size: 15px; font-weight: 500; color: var(--ink-soft); }

.dapp-tabs { display: flex; gap: 2px; padding: 0 14px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.dapp-tab {
  flex: 1; border: none; background: none; padding: 13px 4px;
  font-size: 12.5px; font-weight: 500; color: #9CA3AF; cursor: pointer;
  border-bottom: 2px solid transparent; font-family: inherit;
}
.dapp-tab.is-active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }

.dapp-main { padding: 16px; }
.dapp-intro { margin: 0 0 12px; }

.dapp-card { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 16px; margin-bottom: 12px; }

.dapp-label { display: block; font-size: 12px; color: var(--ink-soft); margin-bottom: 6px; }
.dapp-input, .dapp-textarea, .dapp-select {
  width: 100%; box-sizing: border-box; border: 1px solid var(--line); border-radius: 8px;
  padding: 9px 10px; font-size: 15px; font-family: inherit; color: var(--ink); background: #fff; outline: none;
}
.dapp-input:focus, .dapp-textarea:focus, .dapp-select:focus { border-color: var(--accent); }
.dapp-textarea { resize: vertical; line-height: 1.4; }
.dapp-file { width: 100%; font-size: 13px; }
.dapp-thumb { width: 100%; border-radius: 8px; margin-top: 8px; border: 1px solid var(--line); }

.dapp-btn {
  border: 1px solid var(--line); background: #fff; color: var(--ink); padding: 10px 14px;
  border-radius: 8px; font-size: 14px; cursor: pointer; font-family: inherit;
}
.dapp-btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }
.dapp-btn-primary:disabled { opacity: 0.5; cursor: default; }
.dapp-btn-block { width: 100%; margin-top: 6px; }
.dapp-btn-text { border: none; background: none; color: var(--accent); font-size: 13.5px; font-weight: 500; cursor: pointer; padding: 4px 0; font-family: inherit; }

.dapp-hint { font-size: 13px; color: var(--ink-soft); }
.dapp-error { font-size: 13px; color: #B3453F; margin: 6px 0; }
.dapp-warn { font-size: 12px; color: #B3453F; margin: 4px 0 0; }

.dapp-add-row { display: flex; gap: 8px; }
.dapp-add-row .dapp-input { flex: 1; }

.dapp-section-bar { color: #fff; font-size: 13px; font-weight: 600; padding: 8px 12px; border-radius: 4px; margin: 16px 0 10px; }

.dapp-part-title { font-size: 15px; font-weight: 600; margin-bottom: 10px; }
.dapp-part-min { font-weight: 400; color: var(--ink-soft); }
.dapp-slot { margin-bottom: 8px; }
.dapp-slot:last-child { margin-bottom: 0; }
.dapp-slot-label { display: block; font-size: 11px; color: var(--ink-soft); margin-bottom: 4px; }

.dapp-actions-row { display: flex; gap: 8px; margin-bottom: 10px; }
.dapp-actions-row .dapp-btn { flex: 1; }

.dapp-entry-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
.dapp-entry-num { font-size: 13px; font-weight: 600; color: var(--ink-soft); }
.dapp-mode-switch { display: flex; gap: 6px; }
.dapp-mode-btn {
  flex: 1; border: 1px solid var(--line); background: #fff; color: var(--ink-soft);
  padding: 7px 6px; font-size: 12.5px; border-radius: 5px; cursor: pointer; font-family: inherit;
}
.dapp-mode-btn.is-active { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }
.dapp-week-title-input { font-family: inherit; font-weight: 600; font-size: 15.5px; margin-bottom: 4px; }

.dapp-gender-switch { display: flex; gap: 6px; }
.dapp-gender-btn {
  border: 1px solid var(--line); background: #fff; color: var(--ink-soft);
  padding: 9px 10px; font-size: 13px; border-radius: 8px; cursor: pointer; font-family: inherit;
}
.dapp-gender-btn.is-active { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }

.dapp-person-head { display: flex; justify-content: space-between; align-items: center; cursor: pointer; }
.dapp-person-name { font-size: 15px; font-weight: 600; }
.dapp-chevron { color: var(--ink-soft); }
.dapp-quals { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 10px; }
.dapp-quals-section { font-size: 12px; font-weight: 600; margin: 10px 0 4px; }
.dapp-checkbox-row { display: flex; align-items: center; gap: 8px; font-size: 14px; padding: 4px 0; }
.dapp-efetivo-row { align-items: flex-start; padding-bottom: 10px; margin-bottom: 8px; border-bottom: 1px solid var(--line); }
.dapp-efetivo-row input { margin-top: 3px; }
.dapp-efetivo-hint { font-size: 12px; }
.dapp-remove { margin-top: 10px; }
.dapp-btn-danger { color: #B3453F; }
.dapp-btn-danger-solid { background: #B3453F; border-color: #B3453F; color: #fff; font-weight: 600; }

.dapp-modal-backdrop {
  position: fixed; inset: 0; background: rgba(31, 35, 40, 0.45);
  display: flex; align-items: center; justify-content: center; padding: 20px; z-index: 100;
}
.dapp-modal {
  background: #fff; border-radius: 12px; padding: 20px; max-width: 340px; width: 100%;
  box-shadow: 0 20px 50px rgba(31, 35, 40, 0.25);
}
.dapp-modal-title { font-size: 15px; font-weight: 600; margin-bottom: 8px; }
.dapp-modal-msg { font-size: 14px; color: var(--ink-soft); margin: 0 0 18px; line-height: 1.4; }
.dapp-modal-actions { display: flex; gap: 8px; }
.dapp-modal-actions .dapp-btn { flex: 1; }

.dapp-login {
  min-height: 100%; display: flex; flex-direction: column; justify-content: center;
  padding: 32px 28px; box-sizing: border-box;
}
.dapp-login .dapp-eyebrow { text-align: center; }
.dapp-login-title { font-size: 22px; font-weight: 600; margin: 4px 0 22px; text-align: center; }

.dapp-week-title { font-family: inherit; font-weight: 600; font-size: 18px; margin: 10px 0 2px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.dapp-tag-skip { font-size: 11px; background: #F1F0EC; color: var(--ink-soft); padding: 2px 7px; border-radius: 10px; font-family: inherit; font-weight: 500; }
.dapp-history-card { display: flex; justify-content: space-between; align-items: center; }
.dapp-history-click { cursor: pointer; flex: 1; }
.dapp-history-row { display: flex; justify-content: space-between; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--line); font-size: 14px; }
.dapp-history-title { flex: 1; }
.dapp-history-names { color: var(--ink-soft); text-align: right; }

.dapp-month-header { font-family: inherit; font-size: 15.5px; font-weight: 600; margin-bottom: 6px; }
.dapp-week-row {
  display: flex; justify-content: space-between; align-items: center; cursor: pointer;
  padding: 10px 2px; border-bottom: 1px solid var(--line);
}
.dapp-week-row:last-child { border-bottom: none; }
.dapp-week-row-title { font-size: 14.5px; font-weight: 600; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.dapp-chevron { color: var(--ink-soft); font-size: 18px; }

.dapp-print-area { display: none; }

@media print {
  body * { visibility: hidden; }
  .dapp-print-area, .dapp-print-area * { visibility: visible; }
  .dapp-print-area {
    display: block;
    position: absolute;
    left: 0; top: 0; width: 100%;
    font-family: Georgia, 'Times New Roman', serif;
    color: #1c1b18;
  }
  .print-doc-title { font-size: 18px; margin: 0 0 2px; }
  .print-doc-subtitle { font-size: 12px; color: #55503f; margin: 0 0 14px; font-family: -apple-system, sans-serif; }
  .print-week { page-break-inside: avoid; break-inside: avoid; padding-top: 4px; padding-bottom: 8px; margin-bottom: 10px; border-bottom: 2px solid #bbb; }
  .print-week:last-child { border-bottom: none; }
  .print-week-title {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
    font-variant-numeric: lining-nums;
    font-size: 14px; font-weight: 600; border-bottom: 1px solid #999; padding-bottom: 4px; margin: 0 0 6px;
  }
  .print-no-meeting { font-size: 12px; font-style: italic; color: #55503f; }
  .print-event-box {
    border: 2px solid #8B3A3A; border-radius: 4px; padding: 10px 14px; margin: 4px 0;
    font-family: -apple-system, sans-serif;
  }
  .print-event-label { font-size: 10.5px; font-weight: bold; letter-spacing: 1.5px; color: #8B3A3A; }
  .print-event-name { font-size: 13px; font-weight: 600; margin-top: 2px; }
  .print-section-bar { color: #fff; font-size: 10.5px; font-weight: bold; padding: 3px 8px; margin: 8px 0 4px; font-family: -apple-system, sans-serif; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
  .print-part-row { display: flex; justify-content: space-between; gap: 12px; font-size: 11.5px; padding: 3px 2px; border-bottom: 1px dotted #ccc; break-inside: avoid; font-family: -apple-system, sans-serif; }
  .print-part-title { flex: 1; }
  .print-part-names { font-weight: bold; text-align: right; white-space: nowrap; }
}
`;

/* ---------------------------------------------------------
   Monta o app na página (fora do claude.ai)
--------------------------------------------------------- */
const _rootEl = document.getElementById("root");
if (_rootEl) {
  ReactDOM.createRoot(_rootEl).render(<DesignacoesApp />);
}
