/* global React, ReactDOM, qrcode */
// Agendamento no ar: mesma página aprovada, com links, horários, reservas e Pix vindos de api.php.
const DS = window.AdrianoBarbozaDesignSystem_6f93c0;
const AG = window.AG;
const { NOW, T, PAISES, BY_CODE } = AG;
AG.previa = /[?&]previa=1/.test(location.search);
// Prévia dentro do painel: o editor manda a configuração por mensagem e nada é gravado.
function agPrevHorarios(l, de, ate) {
const out = { dias: {}, cheios: [] }, min = Date.now() + (+l.antecedencia || 0) * 36e5, max = Date.now() + (+l.janela || 90) * 864e5, dur = +l.dur || 60, passo = +l.passo || 30;
for (let d = new Date(de + "T00:00:00"); AG.ymd(d) <= ate; d.setDate(d.getDate() + 1)) {
const livres = [];
((l.horarios || {})[String(d.getDay())] || []).forEach(([a, b]) => {
const am = +a.slice(0, 2) * 60 + +a.slice(3, 5), bm = +b.slice(0, 2) * 60 + +b.slice(3, 5);
for (let t = am; t + dur <= bm; t += passo) { const ts = new Date(d.getFullYear(), d.getMonth(), d.getDate(), Math.floor(t / 60), t % 60); if (ts >= min && ts <= max) livres.push(AG.pad(Math.floor(t / 60)) + ":" + AG.pad(t % 60)); }
});
if (livres.length) out.dias[AG.ymd(d)] = livres;
}
return out;
}
async function agMock(acao, params, body) {
const l = AG.prevLink || {};
await new Promise((ok) => setTimeout(ok, 150));
if (acao === "horarios") return agPrevHorarios(l, params.de, params.ate);
if (acao === "reservar") {
const total = (+l.preco || 0) * Math.max(1, (body.horarios || []).length), pix = total > 0 && l.exigirPix !== false;
return { ok: true, status: pix ? "aguardando_pix" : "confirmado", token: "previa", credito: 0, pix: pix ? { codigo: "00020126580014br.gov.bcb.pix0136PREVIA-NADA-E-COBRADO5204000053039865406" + total.toFixed(2) + "5802BR6304PREV", valor: total, txid: "PREVIA", expira: Math.floor(Date.now() / 1000) + 7200 } : null };
}
if (acao === "verificar_form") return { feito: false };
return { ok: true };
}
const agApiReal = async (acao, params, body) => {
const r = await fetch("api.php?acao=" + acao + (params ? "&" + new URLSearchParams(params) : ""), body ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } : undefined);
const j = await r.json().catch(() => ({}));
if (!r.ok) throw Object.assign(new Error(j.erro || "Erro " + r.status), { code: r.status });
return j;
};
const agApi = (acao, params, body) => AG.previa ? agMock(acao, params, body) : agApiReal(acao, params, body);
const agQr = (s) => { try { const q = qrcode(0, "M"); q.addData(s); q.make(); return q.createSvgTag({ cellSize: 4, margin: 2, scalable: true }); } catch (e) { return ""; } };
// Field overrides on top of DS.Input: 10px radius, 16px text (no iOS zoom),
// 48px min height for touch, and a slightly stronger hairline on off-white.
const agInput = { borderRadius: 10, fontSize: 16, minHeight: 48, borderColor: "rgba(0,0,0,0.28)" };
function AgCTA({ children, disabled = false, style = {}, ...rest }) {
return (
{ if (!disabled) e.currentTarget.style.background = "var(--yellow)"; }}
onMouseLeave={(e) => { e.currentTarget.style.background = "var(--salmon)"; e.currentTarget.style.transform = "scale(1)"; }}
{...rest}>{children}
);
}
function AgGhost({ children, style = {}, ...rest }) {
return {children} ;
}
function IconLock() {
return (
);
}
function IconClock() {
return (
);
}
const IconCheck = ({ s = 14 }) => (
);
// Thin progress line between step numbers; the current number fills black.
const STEP_ORDER = { escolher: 0, dados: 1, pagar: 2, expirou: 2, feito: 3 };
function Steps({ L, step, pago }) {
const labels = pago ? L.etapas4 : L.etapas3;
const idx = pago ? STEP_ORDER[step] : ({ escolher: 0, dados: 1, expirou: 1, pagar: 1, feito: 2 })[step];
const n = labels.length;
return (
{labels.map((l, i) => (
{i < idx ? : i + 1}
{l}
))}
);
}
// Height-animated disclosure; content is inert while closed.
function Collapse({ open, children }) {
return (
);
}
function OptBtn({ open, onClick, children }) {
return (
+ {children}
);
}
function FlagBR() {
return (
);
}
function FlagUK() {
return (
);
}
function LangSwitch({ lang, setLang }) {
return (
setLang("pt")}> PT
setLang("en")}> EN
);
}
// Slot time in the visitor's zone, with Brasília in parentheses when they differ.
function useZone(lang, fuso) {
const tz = fuso === "lisboa" ? "Europe/Lisbon"
: (() => { try { return Intl.DateTimeFormat().resolvedOptions().timeZone || "America/Sao_Paulo"; } catch (e) { return "America/Sao_Paulo"; } })();
const isBRT = AG.sameAsBRT(tz);
const locale = lang === "en" ? "en-GB" : "pt-BR";
const label = (day, s) => {
if (isBRT) return { main: AG.fmtHora(s), brt: null, shift: 0 };
const z = AG.inZone(day, s, tz, locale);
return { main: z.hm, brt: AG.fmtHora(s), shift: z.shift };
};
return { tz, isBRT, label, tzName: tz.replace(/_/g, " ") };
}
function Calendar({ L, month, setMonth, day, setDay, dur }) {
const first = new Date(month.getFullYear(), month.getMonth(), 1);
const days = new Date(month.getFullYear(), month.getMonth() + 1, 0).getDate();
const cells = [];
for (let i = 0; i < first.getDay(); i++) cells.push(null);
for (let i = 1; i <= days; i++) cells.push(new Date(month.getFullYear(), month.getMonth(), i));
const atStart = month.getFullYear() === NOW.getFullYear() && month.getMonth() === NOW.getMonth();
const lim = AG.limite || new Date(NOW.getFullYear(), NOW.getMonth() + 3, 0);
const atEnd = month.getFullYear() * 12 + month.getMonth() >= lim.getFullYear() * 12 + lim.getMonth();
const fdia = (d) => L.dia(d, L.meses, L.dowL);
return (
{L.meses[month.getMonth()]} {month.getFullYear()}
setMonth(new Date(month.getFullYear(), month.getMonth() - 1, 1))}>‹
setMonth(new Date(month.getFullYear(), month.getMonth() + 1, 1))}>›
{L.dow.map((d) =>
{d}
)}
{cells.map((d, i) => {
if (!d) return
;
const n = AG.slotsFor(d, dur).length;
const full = AG.isFull(d, dur);
const on = AG.sameDay(d, day);
return (
setDay(d)}>{d.getDate()}
);
})}
8 {L.legLivre}
8 {L.legLotado}
8 {L.legSem}
);
}
function PhoneField({ L, lang, value, setValue, country, setCountry, error }) {
const isBR = country === "BR";
const onType = (e) => {
const raw = e.target.value;
if (/^\s*\+/.test(raw)) {
const digits = raw.replace(/\D/g, "");
const hit = BY_CODE.find((p) => digits.startsWith(p[3]));
if (hit) { setCountry(hit[0]); const rest = digits.slice(hit[3].length); setValue(hit[0] === "BR" ? AG.maskBR(rest) : AG.maskIntl(rest)); return; }
setValue("+" + digits); return;
}
const d = raw.replace(/\D/g, "");
setValue(isBR ? AG.maskBR(d) : AG.maskIntl(d));
};
return (
{ setCountry(e.target.value); setValue(""); }}>
{PAISES.map((p) => {p[0]} +{p[3]} · {lang === "en" ? p[2] : p[1]} )}
);
}
const URL_OK = (u) => /^https:\/\/\S+\.\S+/.test(u.trim());
function MatInputs({ L, url, desc, setUrl, setDesc, bad, idp }) {
const svc = url.trim() ? AG.svcName(url) : null;
return (
setUrl(e.target.value)}
placeholder="https://" aria-label={L.mat} invalid={bad} aria-invalid={bad} style={agInput} />
setDesc(e.target.value)}
placeholder={L.matDesc} aria-label={L.matDesc} style={agInput} />
{bad ? {L.e.url} : svc ? {svc} : null}
);
}
// Typed links become numbered blocks when added; each block can be edited or removed.
function MaterialsField({ L, items, setItems, draft, setDraft, tried }) {
const [edit, setEdit] = React.useState(null); // { i, url, desc }
const [draftBad, setDraftBad] = React.useState(false);
const add = () => {
if (!URL_OK(draft.url)) { setDraftBad(true); return; }
setItems([...items, { url: draft.url.trim(), desc: draft.desc.trim() }]);
setDraft({ url: "", desc: "" }); setDraftBad(false);
};
const editBad = edit && !URL_OK(edit.url);
return (
{L.mat} · {L.matOpt}
{L.matHint}
{items.length ? (
{items.map((it, i) => (
{L.linkN(i + 1)}
{AG.svcName(it.url) ? {AG.svcName(it.url)} : null}
{edit && edit.i === i ? (
setEdit({ ...edit, url: v })} setDesc={(v) => setEdit({ ...edit, desc: v })} />
{ setItems(items.map((x, j) => (j === i ? { url: edit.url.trim(), desc: edit.desc.trim() } : x))); setEdit(null); }}>{L.salvar}
setEdit(null)}>{L.cancelarEd}
) : (
{it.desc ? {it.desc}
: null}
{it.url}
setEdit({ i, url: it.url, desc: it.desc })}>{L.editar}
{ setItems(items.filter((_, j) => j !== i)); setEdit(null); }}>{L.remover}
)}
))}
) : null}
{items.length < 5 ? (
{ setDraft({ ...draft, url: v }); setDraftBad(false); }} setDesc={(v) => setDraft({ ...draft, desc: v })} />
{tried && draft.url.trim() && !draftBad ? {L.linkPend} : null}
+ {L.matAdd}
) : {L.matMax} }
);
}
function GuestsField({ L, guests, setGuests }) {
const [v, setV] = React.useState("");
const [err, setErr] = React.useState("");
const add = () => {
const e = v.trim();
if (!e) return;
if (!/^\S+@\S+\.\S+$/.test(e)) { setErr(L.convErr); return; }
if (guests.indexOf(e) === -1 && guests.length < 5) setGuests([...guests, e]);
setV(""); setErr("");
};
return (
{L.conv} · {L.matOpt}
{L.convHint}
{guests.length ? (
{guests.map((g) => (
{g} setGuests(guests.filter((x) => x !== g))}>×
))}
) : null}
{guests.length < 5 ? (
{ setV(e.target.value); setErr(""); }}
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); add(); } }}
placeholder="nome@exemplo.com" aria-label={L.conv} invalid={!!err} style={agInput} />
{L.convAdd}
) : null}
{err ? {err} : null}
);
}
function RecurrenceField({ L, cfg, rep, setRep, day, slot, zone, tried }) {
const freqs = cfg.recorrencia.freqs;
const list = rep.on ? AG.series(day, slot, rep.freq, rep.qtd, cfg.dur) : [];
const decide = (i, v) => setRep({ ...rep, dec: { ...rep.dec, [i]: v } });
const reset = (patch) => setRep({ ...rep, ...patch, dec: {} });
const pend = list.some((it, i) => it.status === "ajustado" && !rep.dec[i]);
return (
{L.rep}
{rep.on ? (
{freqs.map((fq) => (
reset({ freq: fq })}>{L.freq[fq]}
))}
{L.repQtd}
reset({ qtd: +e.target.value })}>
{Array.from({ length: cfg.recorrencia.max - 1 }, (_, i) => i + 2).map((n) => {n} )}
{list.map((it, i) => {
const z = it.s ? zone.label(it.d, it.s) : null;
const dec = rep.dec[i];
let right;
if (it.status === "ok") right = {z.main} · {L.repOk} ;
else if (it.status === "sem" || dec === "nao") right = {L.repSem} ;
else if (dec === "sim") right = {z.main} · {L.repOk} ;
else right = (
{L.sugestao(z.main)}
decide(i, "sim")}>{L.aceitar(z.main)}
decide(i, "nao")}>{L.recusar}
);
return ({L.diaCurto(it.d, L.meses)} {right} );
})}
{tried && pend ? {L.repPend} : null}
) : null}
);
}
// Collapsed by default: a count + progress, and a toggle to show each item.
function PendingPanel({ L, lang, st, setSt, modo }) {
const p = AG.PESSOAL;
const total = p.formularios.length + p.tarefas.length;
const feitos = p.formularios.filter((f) => st.forms[f.id]).length + p.tarefas.filter((_, i) => st.tasks[i]).length;
const pend = total - feitos;
const formsOpen = p.formularios.some((f) => !st.forms[f.id]);
const upd = (k, v) => setSt({ ...st, [k]: { ...st[k], ...v } });
// Preview: the first check finds nothing (form not sent yet); the second
// finds the response. In production this reads the Forms response sheet.
const check = async (id) => { try { const j = await agApi("verificar_form", null, { slug: AG.slug, p: AG.p, id }); if (j.feito) upd("forms", { [id]: true }); else upd("miss", { [id]: true }); } catch (e) { upd("miss", { [id]: true }); } };
return (
{L.ola(p.nome)} · {p.projeto[lang]}
{pend ? L.pendDetect(pend) : L.nenhumaPend}
{L.progresso(feitos, total)}
setSt({ ...st, aberto: !st.aberto })}>{st.aberto ? L.ocultar : L.mostrar}
{st.aberto ? (
{L.forms}
{p.formularios.map((f) => (
{f.titulo[lang]}{!st.forms[f.id] && st.miss[f.id] ? {L.verificando} : null}
{st.forms[f.id] ? {L.feitoForm} : (
{L.abrir} ↗
check(f.id)}>{L.verificarItem}
)}
))}
{L.tarefas}
{p.tarefas.map((t, i) => (
{t.titulo[lang]}{t.prazo[lang]}
{ const v = !st.tasks[i]; upd("tasks", { [i]: v }); agApi("tarefa_feita", null, { slug: AG.slug, p: AG.p, id: t.id, feita: v }).catch(() => {}); }}>
{st.tasks[i] ? L.tarefaFeita : L.marcarFeito}
))}
) : null}
{formsOpen ? {modo === "exige" ? L.bloqueado : L.lembretePend}
: null}
);
}
function Payment({ L, preco, pix, onPaid, onExpire }) {
const [until] = React.useState(() => pix.expira * 1000);
const [now, setNow] = React.useState(Date.now());
const [copied, setCopied] = React.useState(false);
React.useEffect(() => { const id = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(id); }, []);
const left = Math.max(0, until - now);
React.useEffect(() => { if (left === 0) onExpire(); }, [left === 0]);
const hh = Math.floor(left / 3600e3), mm = Math.floor((left % 3600e3) / 60e3), ss = Math.floor((left % 60e3) / 1e3);
const code = pix.codigo;
const copy = () => { try { navigator.clipboard.writeText(code); } catch (e) {} setCopied(true); setTimeout(() => setCopied(false), 2000); };
const pct = left / (2 * 3600e3);
return (
{L.pagTit}
{L.restam}
{hh}:{AG.pad(mm)}:{AG.pad(ss)}
{L.pagTxt}
{L.valorPag} R$ {preco.toFixed(2).replace(".", ",")} {L.idPix}: {pix.txid}
{L.copia}
{code}
{copied ? L.copiado : L.copiar}
);
}
function App({ t, cfgL, oferta }) {
const [lang, setLang] = React.useState(() => {
try { const q = new URLSearchParams(location.search).get("lang"); if (q === "en" || q === "pt") return q; } catch (e) {}
if (AG.previa) return "pt";
return (navigator.language || "pt").toLowerCase().startsWith("pt") ? "pt" : "en";
});
const L = T[lang];
const cfg = cfgL;
const zone = useZone(lang, t.fuso);
const titulo = (t.titulo || "").trim() || cfg.titulo[lang];
const subtitulo = (t.subtitulo || "").trim() || cfg.sub[lang];
const presencial = cfg.sala === "presencial";
const preco = t.pago && cfg.preco ? cfg.preco : 0;
const fdia = (d) => L.dia(d, L.meses, L.dowL);
React.useEffect(() => { document.documentElement.lang = lang === "en" ? "en" : "pt-BR"; }, [lang]);
const [tick, setTick] = React.useState(0);
const [res, setRes] = React.useState(null);
const [busy, setBusy] = React.useState(false);
const [erroEnv, setErroEnv] = React.useState("");
const firstAvail = React.useMemo(() => {
for (let i = 0; i < 60; i++) { const d = new Date(NOW.getFullYear(), NOW.getMonth(), NOW.getDate() + i); if (AG.slotsFor(d, cfg.dur).length) return d; }
return null;
}, [cfg.dur, tick]);
const [month, setMonth] = React.useState(new Date(NOW.getFullYear(), NOW.getMonth(), 1));
const [day, setDay] = React.useState(firstAvail);
const [slot, setSlot] = React.useState(null);
const [step, setStep] = React.useState("escolher");
React.useEffect(() => { AG.onUpdate = () => setTick((x) => x + 1); }, []);
React.useEffect(() => {
const a = new Date(month.getFullYear(), month.getMonth(), 1), b = new Date(month.getFullYear(), month.getMonth() + 1, 0), k = AG.ymd(a);
if (AG.carregados[k]) return; AG.carregados[k] = 1;
agApi("horarios", { l: AG.slug, de: AG.ymd(a), ate: AG.ymd(b), ...(AG.o ? { o: AG.o } : {}) })
.then((j) => { Object.assign(AG.AVAIL.dias, j.dias || {}); AG.AVAIL.cheios = AG.AVAIL.cheios.concat(j.cheios || []); setTick((x) => x + 1); })
.catch(() => { AG.carregados[k] = 0; });
}, [month, tick === -1]);
const [f, setF] = React.useState({ nome: (oferta && oferta.nome) || (AG.PESSOAL && AG.PESSOAL.nome) || "", email: (oferta && oferta.email) || (cfgL.pessoal && cfgL.pessoal.email) || "", tel: "", assunto: "", endereco: "", cliente: (cfgL.clientes || []).length === 1 ? cfgL.clientes[0] : "", wpp: true, lgpd: false });
const [pais, setPais] = React.useState("BR");
const [mats, setMats] = React.useState([]);
const [matDraft, setMatDraft] = React.useState({ url: "", desc: "" });
const [guests, setGuests] = React.useState([]);
const [rep, setRep] = React.useState({ on: false, freq: "semanal", qtd: 4, dec: {} });
const [pendSt, setPendSt] = React.useState({ forms: AG.PESSOAL ? Object.fromEntries(AG.PESSOAL.formularios.filter((x) => x.feito).map((x) => [x.id, true])) : {}, tasks: {}, miss: {}, aberto: false });
const formsDone = pendSt.forms;
const [espera, setEspera] = React.useState(null);
const [paid, setPaid] = React.useState(false);
const [tried, setTried] = React.useState(false);
const [opt, setOpt] = React.useState({ rep: false, mat: false, conv: false });
const [maisInfo, setMaisInfo] = React.useState(false);
const prevStep = React.useRef(step);
const dir = STEP_ORDER[step] >= STEP_ORDER[prevStep.current] ? "fwd" : "back";
React.useEffect(() => { prevStep.current = step; }, [step]);
React.useEffect(() => {
setDay(firstAvail); setSlot(null); setStep("escolher"); setEspera(null); setPaid(false); setOpt({ rep: false, mat: false, conv: false });
setRep((r) => ({ ...r, on: false, freq: cfg.recorrencia ? cfg.recorrencia.freqs[0] : "semanal", qtd: 4, dec: {} }));
}, [t.link]);
React.useEffect(() => { if (!day && firstAvail && !oferta) setDay(firstAvail); }, [firstAvail]);
React.useEffect(() => { if (oferta) { const d = new Date(oferta.dia + "T00:00:00"); setMonth(new Date(d.getFullYear(), d.getMonth(), 1)); setDay(d); setSlot({ h: +oferta.hora.slice(0, 2), m: +oferta.hora.slice(3, 5) }); } }, []);
const pendOpen = t.pessoal && AG.PESSOAL.formularios.some((x) => !formsDone[x.id]);
const locked = t.pessoal && t.pendModo === "exige" && pendOpen;
const slots = day ? AG.slotsFor(day, cfg.dur) : [];
const dayFull = day && AG.isFull(day, cfg.dur);
const set = (k) => (e) => setF((o) => ({ ...o, [k]: e.target.type === "checkbox" ? e.target.checked : e.target.value }));
const needAddr = presencial && t.localModo === "pessoa";
const telDigits = f.tel.replace(/\D/g, "");
const matsBad = matDraft.url.trim() !== ""; // typed but not added yet
const errs = {
nome: !f.nome.trim() ? L.e.nome : "",
email: !/^\S+@\S+\.\S+$/.test(f.email) ? L.e.email : "",
tel: (pais === "BR" ? telDigits.length < 10 : telDigits.length < 6) ? L.e.tel : "",
assunto: !f.assunto.trim() ? L.e.assunto : "",
cliente: (cfg.clientes || []).length && !f.cliente ? L.e.cliente : "",
endereco: needAddr && !f.endereco.trim() ? L.e.end : "",
lgpd: !f.lgpd ? L.e.lgpd : "",
};
const ok = Object.values(errs).every((x) => !x) && !(cfg.materiais && opt.mat && matsBad) && !(rep.on && cfg.recorrencia && slot && AG.series(day, slot, rep.freq, rep.qtd, cfg.dur).some((x, i) => x.status === "ajustado" && !rep.dec[i]));
const calLinks = () => {
const st = new Date(Date.UTC(day.getFullYear(), day.getMonth(), day.getDate(), slot.h + 3, slot.m));
const en = new Date(st.getTime() + cfg.dur * 60000);
const z = (x) => x.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
const text = titulo + " · Adriano Barboza";
const g = "https://calendar.google.com/calendar/render?action=TEMPLATE&text=" + encodeURIComponent(text) + "&dates=" + z(st) + "/" + z(en) + "&location=" + encodeURIComponent(local);
const o = "https://outlook.live.com/calendar/0/deeplink/compose?subject=" + encodeURIComponent(text) + "&startdt=" + st.toISOString() + "&enddt=" + en.toISOString() + "&location=" + encodeURIComponent(local);
const ics = ["BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//Adriano Barboza//Agendamento//PT", "BEGIN:VEVENT", "UID:" + z(st) + "@adrianobarboza.com",
"DTSTAMP:" + z(new Date()), "DTSTART:" + z(st), "DTEND:" + z(en), "SUMMARY:" + text, "LOCATION:" + local, "END:VEVENT", "END:VCALENDAR"].join("\r\n");
return { g, o, a: "data:text/calendar;charset=utf-8," + encodeURIComponent(ics) };
};
const salaTxt = presencial ? (t.localModo === "pessoa" ? L.presPessoa : L.presFixo + " · " + cfg.endereco) : L.sala[cfg.sala];
const local = presencial ? (t.localModo === "pessoa" ? (f.endereco || L.presPessoa) : cfg.endereco) : L.sala[cfg.sala];
const mapsUrl = "https://www.google.com/maps/search/?api=1&query=" + encodeURIComponent(f.endereco || "");
const nMats = mats.length;
const hora = (d, s) => { const z = zone.label(d, s); return z.main + (z.shift ? " (" + (z.shift > 0 ? L.diaSeg : L.diaAnt) + ")" : "") + (z.brt ? " · " + z.brt + " " + L.brasilia : ""); };
const repList = rep.on && cfg.recorrencia && slot ? AG.series(day, slot, rep.freq, rep.qtd, cfg.dur) : [];
const repN = repList.filter((x, i) => x.status === "ok" || (x.status === "ajustado" && rep.dec[i] === "sim")).length;
const repPend = repList.some((x, i) => x.status === "ajustado" && !rep.dec[i]);
const enviar = async () => {
setBusy(true); setErroEnv("");
const lista = rep.on && cfg.recorrencia ? repList.map((x, i) => x.s && (x.status === "ok" || (x.status === "ajustado" && rep.dec[i] === "sim")) ? { dia: AG.ymd(x.d), hora: AG.fmtHora(x.s) } : null).filter(Boolean) : [{ dia: AG.ymd(day), hora: AG.fmtHora(slot) }];
try {
const tel = f.tel ? "+" + (PAISES.find((p) => p[0] === pais) || ["", "", "", "55"])[3] + " " + f.tel : "";
const j = await agApi("reservar", null, { slug: AG.slug, p: AG.p || "", oferta: AG.o || "", nome: f.nome, email: f.email, tel, wpp: f.wpp, cliente: f.cliente, pauta: f.assunto, endereco: f.endereco, lgpd: f.lgpd, lang,
materiais: opt.mat ? mats : [], convidados: opt.conv ? guests : [], horarios: lista });
setRes(j); setStep(j.status === "aguardando_pix" ? "pagar" : "feito");
} catch (e) {
setErroEnv(e.message);
if (e.code === 409) { AG.carregados = {}; AG.AVAIL.dias = {}; AG.AVAIL.cheios = []; setMonth(new Date(month.getTime())); }
}
setBusy(false);
};
const left = (
{L.eyebrow} · {cfg.dur} {L.min}
{titulo}{subtitulo ? {subtitulo} : null}
{cfg.dur} {L.min} · {salaTxt}{preco ? " · " + L.valorV(preco) : ""}
setMaisInfo(!maisInfo)}>
+ {maisInfo ? L.ocultarDet : L.detalhes}
{cfg.texto[lang]}
{L.com} Dr. Adriano Barboza, Ph.D., BCBA-D
{L.dur} {cfg.dur} {L.minutos}
{L.local} {salaTxt}
{preco ? {L.valor} {L.valorV(preco)} : null}
{L.antecedencia} {L.antecedenciaV}
{L.cancel} {L.cancelV}
Adriano Barboza · {L.sign}
);
let right;
if (step === "escolher") {
let body;
if (locked) body = {L.bloqueado}
;
else if (dayFull) {
body = espera && AG.sameDay(espera, day) ? (
) : (
{L.espTit}
{L.espTxt}
{L.espDados}
{[["nome", "text", "name"], ["email", "email", "email"]].map(([k, ty, ac]) => )}
{erroEnv ?
{erroEnv} : null}
{
if (!f.nome.trim()) { setErroEnv(L.e.nome); return; } if (!/^\S+@\S+\.\S+$/.test(f.email)) { setErroEnv(L.e.email); return; }
setBusy(true); setErroEnv("");
try { await agApi("espera", null, { slug: AG.slug, dia: AG.ymd(day), nome: f.nome, email: f.email, lang }); setEspera(day); } catch (e) { setErroEnv(e.message); }
setBusy(false);
}}>{L.espEntrar}
);
} else if (day && slots.length) {
body = (
{slots.map((s, i) => {
const on = slot && slot.h === s.h && slot.m === s.m;
const z = zone.label(day, s);
return (
setSlot(s)} style={{ animationDelay: Math.min(i, 14) * 18 + "ms" }}>
{z.main}{z.shift ? {z.shift > 0 ? "+1" : "−1"} : null}
{z.brt ? {z.brt} {L.brasilia} : null}
);
})}
);
} else body = {L.vazio}
;
right = (
{ setDay(d); setSlot(null); }} dur={cfg.dur} />
{zone.isBRT ? L.tzBRT : L.tzVoce(zone.tzName)}
{L.janelaTxt(cfg.janela)}
{day ? fdia(day) : L.escolhaDia}
{locked ? : null}
{body}
{!locked && !dayFull ? (
setStep("dados")}>
{slot ? L.continuar(zone.label(day, slot).main) : L.escolhaHor}
) : null}
);
} else if (step === "dados") {
right = (
);
} else if (step === "pagar") {
right = { try { await agApi("ja_paguei", null, { token: res.token }); } catch (e) {} setPaid(true); setStep("feito"); }} onExpire={() => setStep("expirou")} />;
} else if (step === "expirou") {
right = (
{L.expirou}
{ setStep("escolher"); setSlot(null); }}>{L.novoHor}
);
} else {
const cl = calLinks();
right = (
{paid ? L.reservaFeita : L.feito}
{paid ?
{L.aguardando}
:
{L.enviadoPara}{f.email || L.seuEmail} {L.feitoTxt(presencial)}
}
{L.quando} {fdia(day)}, {hora(day, slot)}{repN > 1 ? " · " + L.repResumo(repN) : ""}
{L.reuniao} {titulo}
{f.cliente ? {L.cliente} {f.cliente} : null}
{L.local} {local}
{guests.length ? {L.convidados} {guests.join(", ")} : null}
{cfg.materiais && nMats ? {L.mat} {L.materiaisN(nMats)} : null}
{res && res.credito ? {L.credito} R$ {res.credito.toFixed(2).replace(".", ",")} : null}
{preco && res && res.pix ? {L.pagamento} R$ {preco.toFixed(2).replace(".", ",")} · {L.statusPag.aguardando} : null}
{L.lembretes} {L.lembV(f.wpp)}
{L.janelaTxt(cfg.janela)}
{L.cancelTxt}
{ setStep("escolher"); setSlot(null); setTried(false); setPaid(false); setRes(null); }}>{L.proximos}
);
}
return (
{left}
);
}
function AgFrame({ lang, setLang, children }) {
const L = T[lang];
React.useEffect(() => { document.documentElement.lang = lang === "en" ? "en" : "pt-BR"; }, [lang]);
return (
{children}
agendar.adrianobarboza.com {L.lgpdFoot}
);
}
const agLang0 = () => { try { const q = new URLSearchParams(location.search).get("lang"); if (q === "en" || q === "pt") return q; } catch (e) {} return (navigator.language || "pt").toLowerCase().startsWith("pt") ? "pt" : "en"; };
// Página de gerenciar (link do e-mail): cancelar ou remarcar, até 48 h antes.
function Gerenciar({ r, token }) {
const [lang, setLang] = React.useState(agLang0);
const L = T[lang];
const [st, setSt] = React.useState("ver");
const [erro, setErro] = React.useState("");
const [busy, setBusy] = React.useState(false);
const [, setTick] = React.useState(0);
const [month, setMonth] = React.useState(new Date(NOW.getFullYear(), NOW.getMonth(), 1));
const [day, setDay] = React.useState(null);
const [slot, setSlot] = React.useState(null);
const tit = r.titulo ? (r.titulo[lang] || r.titulo.pt) : "";
const ini = new Date(r.inicio * 1000);
React.useEffect(() => {
if (st !== "remarcar") return;
AG.slug = r.slug;
(async () => {
try {
if (!AG.limite) { const c = await agApi("link", { l: r.slug }); AG.limite = new Date(c.limite + "T12:00:00"); }
const a = new Date(month.getFullYear(), month.getMonth(), 1), b = new Date(month.getFullYear(), month.getMonth() + 1, 0);
const j = await agApi("horarios", { l: r.slug, de: AG.ymd(a), ate: AG.ymd(b) });
Object.assign(AG.AVAIL.dias, j.dias || {}); AG.AVAIL.cheios = []; setTick((x) => x + 1);
} catch (e) { setErro(e.message); }
})();
}, [st, month]);
const acao = async (nome, body, prox) => { setErro(""); setBusy(true); try { await agApi(nome, null, { token, ...body }); setSt(prox); } catch (e) { setErro(e.message); } setBusy(false); };
const slots = day ? AG.slotsFor(day) : [];
let corpo;
if (st === "cancelado" || st === "remarcado") corpo = (
{st === "cancelado" ? L.status.cancelado : L.feito}
{st === "cancelado" ? L.gerCancelado : L.gerRemarcado}
{st === "cancelado" && r.valor > 0 && r.status === "confirmado" ?
{L.gerCredito}
: null}
);
else if (st === "remarcar") corpo = (
{ setDay(d); setSlot(null); }} dur={r.dur} />
{day ? L.dia(day, L.meses, L.dowL) : L.escolhaDia}
{day && slots.length ? {slots.map((s) => { const on = slot && slot.h === s.h && slot.m === s.m; return setSlot(s)}>{AG.fmtHora(s)} ; })}
: day ? {L.vazio}
: null}
{erro ? {erro}
: null}
acao("remarcar", { dia: AG.ymd(day), hora: AG.fmtHora(slot) }, "remarcado")}>{slot ? L.gerNovo : L.escolhaHor}
setSt("ver")}>{L.voltar}
);
else corpo = (
{tit} · {L.dia(ini, L.meses, L.dowL)}, {AG.pad(ini.getHours())}:{AG.pad(ini.getMinutes())} ({L.brasilia}){L.status[r.status] || r.status}
{erro ?
{erro}
: null}
{r.podeMudar ? (
setSt("remarcar")}>{L.gerRemarcar}
{ if (confirm(L.gerConfirmaCancel)) acao("cancelar", {}, "cancelado"); }}>{L.gerCancelar}
) :
{["confirmado", "pix_informado", "aguardando_pix"].indexOf(r.status) !== -1 ? L.gerSemPrazo : L.gerInativo}
}
);
return ;
}
// Página inicial: só os links marcados como públicos no painel. Os outros só abrem por quem recebeu o endereço.
function AgLista({ links }) {
const [lang, setLang] = React.useState(agLang0);
const L = T[lang];
return (
{L.eyebrow}
{links.length ?
: {lang === "en" ? "Use the booking link you received." : "Use o link de agendamento que você recebeu."}
}
);
}
function AgErro({ msg }) {
const [lang, setLang] = React.useState(agLang0);
return {lang === "en" ? "Link not available" : "Link indisponível"} {msg}
;
}
function agMap(c) { const s = (v) => typeof v === "string" ? { pt: v, en: v } : { pt: (v && v.pt) || "", en: (v && (v.en || v.pt)) || "" }; return { ...c, titulo: s(c.titulo), sub: s(c.sub), texto: s(c.texto) }; }
function Root() {
const qs = new URLSearchParams(location.search);
const slug = (location.pathname.split("/").filter(Boolean).pop() || "").replace(/\.html$/, "");
const [st, setSt] = React.useState(null);
React.useEffect(() => {
if (!AG.previa) return;
const ouvir = (e) => {
if (!e.data || e.data.tipo !== "agp-previa") return;
const l = e.data.link || {}, hoje = new Date();
AG.prevLink = l; AG.slug = e.data.slug || "previa"; AG.p = ""; AG.o = ""; AG.PESSOAL = null;
AG.limite = new Date(hoje.getTime() + (+l.janela || 90) * 864e5);
AG.AVAIL.dias = {}; AG.AVAIL.cheios = []; AG.carregados = {};
AG.series = (day, slot, freq, count) => Array.from({ length: count }, (_, i) => { const d = freq === "mensal" ? new Date(day.getFullYear(), day.getMonth() + i, day.getDate()) : new Date(day.getTime() + i * (freq === "quinzenal" ? 14 : 7) * 864e5); const ok = (agPrevHorarios(l, AG.ymd(d), AG.ymd(d)).dias[AG.ymd(d)] || []).includes(AG.fmtHora(slot)); return { d, s: ok || !i ? slot : null, status: ok || !i ? "ok" : "sem" }; });
setSt({ cfg: agMap({ ...l, slug: AG.slug, clientes: l.clientes || [], hoje: AG.ymd(hoje), limite: AG.ymd(AG.limite) }), v: Date.now() });
};
window.addEventListener("message", ouvir);
if (window.parent !== window) window.parent.postMessage({ tipo: "agp-pronto" }, "*");
return () => window.removeEventListener("message", ouvir);
}, []);
React.useEffect(() => { if (AG.previa) return; (async () => {
try {
if (qs.get("r")) { setSt({ manage: await agApi("reserva", { t: qs.get("r") }) }); return; }
if (!slug || slug === "index") { const j = await agApi("lista"); setSt({ lista: j.links || [] }); return; }
const s0 = slug;
const c = await agApi("link", { l: s0, ...(qs.get("p") ? { p: qs.get("p") } : {}), ...(qs.get("o") ? { o: qs.get("o") } : {}) });
AG.slug = s0; AG.p = qs.get("p") || ""; AG.o = c.oferta ? qs.get("o") : ""; AG.limite = new Date(c.limite + "T12:00:00");
if (c.pessoal) AG.PESSOAL = { nome: c.pessoal.nome, projeto: { pt: c.pessoal.projeto, en: c.pessoal.projeto },
formularios: c.pessoal.formularios.map((x) => ({ ...x, titulo: { pt: x.titulo, en: x.titulo } })),
tarefas: c.pessoal.tarefas.map((x) => ({ ...x, titulo: { pt: x.titulo, en: x.titulo }, prazo: { pt: x.prazo || "", en: x.prazo || "" } })) };
setSt({ cfg: agMap(c) });
} catch (e) { setSt({ erro: e.message }); }
})(); }, []);
if (!st) return {AG.previa ? "Abrindo a prévia…" : "Carregando…"}
;
if (st.erro) return ;
if (st.manage) return ;
if (st.lista) return ;
const c = st.cfg;
const t = { link: c.slug, localModo: c.localModo, titulo: "", subtitulo: "", pessoal: !!c.pessoal, pendModo: c.pessoal ? c.pessoal.modo : "lembrete", pago: c.preco > 0, fuso: "auto" };
return {AG.previa ? Prévia do rascunho · nada é gravado nem enviado
: null} ;
}
ReactDOM.createRoot(document.getElementById("root")).render( );