`; baixar(nomeArquivoPar(p) + ".doc", "" + doc, "application/msword"); } function imprimirPar(p) { const f = document.createElement("iframe"); f.style.cssText = "position:fixed;right:0;bottom:0;width:0;height:0;border:0"; document.body.appendChild(f); f.contentDocument.open(); f.contentDocument.write(`${esc(p.titulo || "Parecer")}${htmlParecer(p)}`); f.contentDocument.close(); setTimeout(() => { f.contentWindow.focus(); f.contentWindow.print(); setTimeout(() => f.remove(), 2000); }, 300); } function textoPuroPar(p) { return (p.titulo || "") + "\n\n" + (p.secoes || []).map((s, i) => `${i + 1}. ${s.titulo}\n${s.texto}`).join("\n\n"); } function telaEditorPar(v) { const P = S.par, p = S.d.pareceres.find((x) => x.id === P.id); if (!p) { P.tela = "lista"; return vPareceres(v); } const m = MODELOS_PAR[p.tipo] || { nome: p.tipo }; v.innerHTML = `

${esc(m.nome)} · ${esc(nomeEnt(p.entidade_id))}${p.alvo ? " · " + esc(p.alvo) : ""} · ${p.status === "final" ? "final" : "rascunho"}

Este texto foi escrito por IA a partir do que você contou. Leia e confira cada seção antes de assinar ou enviar. Onde aparecer "[confirmar: …]" falta uma informação.

${(p.secoes || []).map((s, i) => `

${i + 1}. ${esc(s.titulo)}

`).join("")}

`; const aplicar = () => { p.titulo = $("#eTit").value; v.querySelectorAll("[data-sec]").forEach((t) => (p.secoes[+t.dataset.sec].texto = t.value)); }; const salvar = async (extra) => { aplicar(); await Store.update("pareceres", p.id, { titulo: p.titulo, secoes: p.secoes, atualizado_em: new Date().toISOString(), ...(extra || {}) }); }; $("#eVolta").onclick = async () => { aplicar(); P.tela = "lista"; vPareceres(v); }; const sv = async () => { try { await salvar(); toast("Salvo."); } catch (e) { toast("Não consegui salvar."); } }; $("#eSalvar").onclick = sv; $("#eSalvar2").onclick = sv; $("#eWord").onclick = () => { aplicar(); baixarWordPar(p); }; $("#ePdf").onclick = () => { aplicar(); imprimirPar(p); toast('Na janela de impressão, escolha "Salvar como PDF".'); }; $("#eCopiar").onclick = () => { aplicar(); navigator.clipboard?.writeText(textoPuroPar(p)); toast("Texto copiado."); }; $("#eFinal").onclick = async () => { await salvar({ status: p.status === "final" ? "rascunho" : "final" }); telaEditorPar(v); }; $("#eExcl").onclick = async () => { if (confirm("Excluir este parecer? Não dá para desfazer.")) { await Store.remove("pareceres", [p.id]); P.tela = "lista"; toast("Parecer excluído."); vPareceres(v); } }; v.querySelectorAll("[data-ref]").forEach((b) => (b.onclick = async () => { aplicar(); const i = +b.dataset.ref, sec = p.secoes[i], ins = v.querySelector(`[data-ins="${i}"]`).value.trim(); if (!sb) return toast("Refazer funciona só na versão publicada (precisa do Supabase)."); b.disabled = true; b.textContent = "Reescrevendo…"; try { const mapa = p.sem_nomes ? Core.montarMapa(p.respostas?.nomes || []) : [], ps = (t) => Core.pseudonimizar(t, mapa); const { data, error } = await sb.functions.invoke("parecer", { body: { acao: "refazer", modelo_nome: m.nome, alvo: ps(nomeEnt(p.entidade_id) + (p.alvo ? " — " + p.alvo : "")), secao_titulo: sec.titulo, texto_atual: ps(sec.texto), instrucao: ps(ins), relato: ps(p.relato || "") } }); if (error) { let d = ""; try { const j = await error.context.json(); d = (j.erro || "") + (j.detalhe ? " (" + j.detalhe + ")" : ""); } catch {} throw new Error(d || error.message); } if (data?.erro) throw new Error(data.erro); sec.texto = Core.restaurar(data.texto || sec.texto, mapa); v.querySelector(`[data-sec="${i}"]`).value = sec.texto; await salvar(); telaEditorPar(v); toast("Seção reescrita."); } catch (e) { b.disabled = false; b.textContent = "Refazer esta seção"; toast("Não consegui: " + (e.message || "erro").slice(0, 200)); } })); } function blocoAgendado(c, di) { if (c !== "empreender") return ""; const ciclo = S.cicloSel, itens = agendadoSemRegistro().filter((x) => cicloDe(x.data, di).id === ciclo); if (!itens.length) return ""; const aconteceu = itens.filter((x) => jaPassou(x.data, x.fim)).length; return `

Na agenda, ainda sem lançamento · ${itens.length} item(ns)${aconteceu ? ", " + aconteceu + " já aconteceu" : ""}

Compromissos de trabalho e sugestões confirmadas. Quando acontecerem, clique em Registrar: o formulário já vem com data, horário e associação, e você completa com quem foi e o que foi feito.

${itens.map((x) => { const p = jaPassou(x.data, x.fim); return ``; }).join("")}
DataHorárioAssociaçãoCompromissoSituação
${fmtData(x.data)}${hm(x.ini)}–${hm(x.fim)}${esc(nomeEnt(x.entidade_id))}${esc(x.titulo)}${p ? "Já aconteceu: registre" : "Vai acontecer"} ${x.status}
`; } function vRelatorio(v) { const c = S.contrato, di = contrato()?.ciclo_dia_inicio || 11, hoje = hojeISO(); const ciclos = [...new Set(S.d.atividades.concat(S.d.diario).filter((a) => a.contrato_id === c).map((a) => cicloDe(a.data, di).id).concat([cicloDe(hoje, di).id]))].sort(); S.cicloSel = S.cicloSel && ciclos.includes(S.cicloSel) ? S.cicloSel : cicloDe(hoje, di).id; const rows = S.d.atividades.filter((a) => a.contrato_id === c && cicloDe(a.data, di).id === S.cicloSel).sort((a, b) => a.data.localeCompare(b.data) || a.inicio.localeCompare(b.inicio)); const pend = S.d.diario.filter((d) => d.contrato_id === c && !d.atividade_id && cicloDe(d.data, di).id === S.cicloSel); const porDia = {}; rows.forEach((a) => (porDia[a.data] = (porDia[a.data] || 0) + horasAt(a))); const probl = []; for (const [d, h] of Object.entries(porDia)) { if (h > 10) probl.push(fmtData(d) + ": " + f1(h) + " h (acima de 10 h)"); else if (h === 10) probl.push(fmtData(d) + ": exatamente 10 h"); if (!isUtil(d, feriados())) probl.push(fmtData(d) + ": fora de dia útil"); } const dias10 = Object.values(porDia).filter((h) => h >= 10).length; if (dias10 > 1) probl.push("Mais de um dia com 10 h."); const tot = rows.reduce((s, a) => s + horasAt(a), 0); v.innerHTML = `

Relatório do ciclo

${f1(tot)} h em ${rows.length} lançamentos${pend.length ? ` · ${pend.length} registros do diário (${f1(pend.reduce((s, d) => s + d.duracao_min, 0) / 60)} h) ainda não entraram ` : ""}.

${probl.length ? `` : ""}

${blocoAgendado(c, di)}
${rows.map((a) => ``).join("")}
DataInícioFimFormatoAssociaçãoAssuntoCom quemDescriçãoConf.
${fmtData(a.data)}${esc(a.inicio.slice(0, 5))}${esc(a.fim.slice(0, 5))}${esc(a.formato)}${esc(nomeEnt(a.entidade_id))}${esc(a.categoria || "")}${a.origem === "rascunho_retro" ? ' rascunho' : ""}${esc(a.com_quem || "")}${esc(a.descricao || "")}
${a.origem === "rascunho_retro" ? ` ` : ""} ${a.origem === "rascunho_retro" ? ` ` : ""}
`; $("#rCic").onchange = (e) => { S.cicloSel = e.target.value; vRelatorio(v); }; v.querySelectorAll("[data-cf]").forEach((cb) => (cb.onchange = () => Store.update("atividades", cb.dataset.cf, { conferido: cb.checked }))); v.querySelectorAll("[data-cp]").forEach((b) => (b.onclick = () => { const a = S.d.atividades.find((x) => x.id === b.dataset.cp); navigator.clipboard?.writeText(linhaTxt(a)); toast("Copiado."); })); const abrirForm = (a, pre) => formAtividade(a, () => vRelatorio(v), pre); $("#verDiario") && ($("#verDiario").onclick = () => { S.dcic = S.cicloSel; S.dfil = "pend"; S.tab = "diario"; render(); window.scrollTo(0, 0); }); v.querySelectorAll("[data-reg]").forEach((b) => (b.onclick = () => { const it = agendadoSemRegistro().find((x) => x.id === b.dataset.reg); if (!it) return; abrirForm(null, { data: it.data, inicio: hm(it.ini), fim: hm(it.fim), formato: it.formato || "presencial", entidade_id: it.entidade_id || null, categoria: it.categoria || null, origem_ref: it.id }); })); $("#novo").onclick = () => abrirForm(null); v.querySelectorAll("[data-ed]").forEach((b) => (b.onclick = () => abrirForm(S.d.atividades.find((x) => x.id === b.dataset.ed)))); v.querySelectorAll("[data-ap]").forEach((b) => (b.onclick = async () => { const a = S.d.atividades.find((x) => x.id === b.dataset.ap); if (!a.com_quem || !a.descricao) { toast("Antes de aprovar, preencha 'Com quem' e a descrição (botão Editar)."); return abrirForm(a); } await Store.update("atividades", a.id, { origem: "retro_aprovado", conferido: true }); toast("Aprovada: virou lançamento oficial."); vRelatorio(v); })); v.querySelectorAll("[data-rm]").forEach((b) => (b.onclick = async () => { if (confirm("Remover este item do rascunho?")) { await Store.remove("atividades", [b.dataset.rm]); vRelatorio(v); } })); $("#xlsx").onclick = () => exportarXlsx(rows, S.cicloSel); $("#csv").onclick = () => baixar("relatorio-" + S.cicloSel + ".csv", "Data;Início;Fim;Duração;Formato;Associação/Núcleo;Assunto;Com quem;Descrição\n" + rows.map((a) => [a.data.split("-").reverse().join("/"), a.inicio.slice(0, 5), a.fim.slice(0, 5), f1(horasAt(a)), a.formato, nomeEnt(a.entidade_id) + (a.nucleo ? " / " + a.nucleo : ""), a.categoria, a.com_quem, a.descricao].map((x) => '"' + String(x ?? "").replace(/"/g, '""') + '"').join(";")).join("\n"), "text/csv"); $("#comp").onclick = async () => { const somaNovos = {}; pend.forEach((d) => (somaNovos[d.data] = (somaNovos[d.data] || 0) + d.duracao_min / 60)); const estouram = Object.keys(somaNovos).filter((d) => (porDia[d] || 0) + somaNovos[d] >= 10); if (estouram.length && !confirm("Ao compilar, estes dias chegam a 10 h ou mais: " + estouram.map((d) => fmtData(d) + " (" + f1((porDia[d] || 0) + somaNovos[d]) + " h)").join(", ") + ". Compilar mesmo assim?")) return; // Agrupa por dia + associação + assunto; a duração soma e o início é o do primeiro registro do grupo const g = {}; pend.forEach((d) => { const k = [d.data, d.entidade_id, d.categoria].join("|"); (g[k] = g[k] || []).push(d); }); for (const k in g) { const l = g[k].sort((a, b) => a.hora.localeCompare(b.hora)), min = l.reduce((s, d) => s + d.duracao_min, 0), ini = parseHM(l[0].hora); const quem = [...new Set(l.map((d) => d.com_quem).filter(Boolean))].join(", "), nota = l.map((d) => d.nota).filter(Boolean).join(" "); const [a] = await Store.insert("atividades", { contrato_id: c, data: l[0].data, inicio: hm(ini), fim: hm(Math.min(1439, ini + min)), formato: "online", entidade_id: l[0].entidade_id, nucleo: l[0].nucleo, categoria: l[0].categoria, com_quem: quem, descricao: (l.length > 1 ? "Atendimentos ao longo do dia. " : "") + nota, origem: "diario", conferido: false }); for (const d of l) await Store.update("diario", d.id, { atividade_id: a.id }); } toast("Diário compilado. Confira cada linha."); vRelatorio(v); }; } function formAtividade(a, depois, pre) { const box = $("#edBox"); const n = !a; a = a || Object.assign({ data: hojeISO(), inicio: "09:00", fim: "10:00", formato: "online", origem: "relatorio" }, pre || {}); const ents = entsDe(S.contrato).map((e) => ({ v: e.id, t: e.nome })), cats = S.d.categorias.filter((x) => x.contrato_id === S.contrato).map((x) => ({ v: x.nome, t: x.nome })); box.innerHTML = `

${n ? "Novo lançamento" : "Editar lançamento"}${a.origem === "rascunho_retro" ? ' rascunho' : ""}

${a.origem === "rascunho_retro" ? '' : ""}

`; box.scrollIntoView({ behavior: "smooth" }); const ler = () => ({ data: $("#fData").value, inicio: $("#fIni").value, fim: $("#fFim").value, formato: $("#fFmt").value, entidade_id: $("#fEnt").value || null, categoria: $("#fCat").value || null, nucleo: $("#fNuc").value || null, com_quem: $("#fQuem").value || null, descricao: $("#fDesc").value || null }); const valida = (d) => { if (!d.data || !d.inicio || !d.fim || d.fim <= d.inicio) { toast("Confira data e horários (o fim precisa ser depois do início)."); return false; } return true; }; const checa = () => { const d = ler(); return avisoDia(d.data, d.inicio ? parseHM(d.inicio) : 0, d.fim ? parseHM(d.fim) : 0, S.contrato, n ? null : a.id); }; const mostra = () => { $("#fAv").innerHTML = checa().html; }; box.querySelectorAll("input,select").forEach((el) => (el.onchange = mostra)); mostra(); const confirmaLimite = () => { const r = checa(); return r.nivel < 2 || r.total === undefined || confirm("Este lançamento deixa o dia com " + f1(r.total) + " h (limite: abaixo de 10 h). Salvar mesmo assim?"); }; $("#fCancel").onclick = () => (box.innerHTML = ""); $("#fSalvar").onclick = async () => { const d = ler(); if (!valida(d)) return; if (!confirmaLimite()) return; if (n) await Store.insert("atividades", { ...d, contrato_id: S.contrato, origem: a.origem_ref ? "agenda" : "relatorio", origem_ref: a.origem_ref || null, conferido: false }); else await Store.update("atividades", a.id, d); toast("Salvo."); depois(); }; $("#fAprovar") && ($("#fAprovar").onclick = async () => { const d = ler(); if (!valida(d)) return; if (!confirmaLimite()) return; if (!d.com_quem || !d.descricao) return toast("Para aprovar, preencha 'Com quem' e a descrição."); await Store.update("atividades", a.id, { ...d, origem: "retro_aprovado", conferido: true }); toast("Aprovada: virou lançamento oficial."); depois(); }); } const linhaTxt = (a) => `${a.data.split("-").reverse().join("/")} · ${a.inicio.slice(0, 5)}–${a.fim.slice(0, 5)} · ${a.formato}\n${nomeEnt(a.entidade_id)}${a.nucleo ? " / " + a.nucleo : ""}\n${a.categoria || ""}\nCom: ${a.com_quem || ""}\n${a.descricao || ""}`; // ---------------- AJUSTES ---------------- function vAjustes(v) { v.innerHTML = `

Importar agenda (.ics)

O arquivo é lido no seu aparelho. Compromissos pessoais ou de outros assuntos são salvos apenas como "Ocupado" (sem título). Só o que for do Empreender, da Cidade Empreendedora e da COMAC mantém o nome.

Google Agenda

${(() => { const u = cfg("ultima_sync", null); if (!u) return 'Sincronização automática ainda não instalada. Enquanto isso, importe o arquivo .ics acima.'; const m = Math.round((Date.now() - Date.parse(u)) / 60000); return (m > 45 ? '' : '') + "Última sincronização: " + (m < 1 ? "agora há pouco" : m < 60 ? "há " + m + " min" : "há " + Math.round(m / 60) + " h") + ""; })()}

A cada poucos minutos o Google envia seus compromissos ao app (o que não é de trabalho vai só como "Ocupado") e o app cria na agenda "Empreender – Planejamento" as sugestões que você confirmou. Se você mover um desses eventos no Google, o app acompanha.

Preferências

Limite do contrato: máximo de 10 h por dia, nunca 10 h todos os dias, só dias úteis.

Conta

Conectado como ${esc(S.user?.email || "")}.

`; $("#ics").onchange = async (e) => { const f = e.target.files[0]; if (!f) return; const txt = await f.text(), hoje = hojeISO(); const ev = Core.eventosClassificados(txt, hoje, addDays(hoje, 120)); if (!ev.length) return ($("#icsMsg").textContent = "Nenhum evento encontrado no período."); const antigos = S.d.eventos.filter((x) => x.data >= hoje).map((x) => x.id); if (!DEMO) { if (antigos.length) { const { error } = await sb.from("eventos").delete().in("id", antigos); if (error) return toast("Erro ao atualizar."); } const { error } = await sb.from("eventos").insert(ev); if (error) return toast("Erro ao importar."); } S.d.eventos = S.d.eventos.filter((x) => x.data < hoje).concat(ev); $("#icsMsg").textContent = ev.length + " compromissos importados (" + ev.filter((x) => x.tipo === "ocupado").length + " como 'Ocupado')."; }; $("#cSalvar").onclick = async () => { const val = $("#cAlvo").value; if (!DEMO) await sb.from("config").upsert({ chave: "carga_alvo", valor: val }); const r = S.d.config.find((x) => x.chave === "carga_alvo"); if (r) r.valor = val; else S.d.config.push({ chave: "carga_alvo", valor: val }); toast("Salvo."); }; $("#sair").onclick = async () => { if (sb) await sb.auth.signOut(); else location.reload(); }; } boot();