/* ============================================================ AGENDA — Login da empresa + Gestão de usuários ============================================================ */ /* ---------------- TELA DE LOGIN ---------------- Cada empresa entra pelo próprio endereço (/). O login desta tela só vale para a empresa do endereço — credenciais de outra empresa não entram aqui. Sem endereço na URL, pede o endereço. */ const LEGACY_LOGIN_REMEMBER_PREFIX = 'ag-login-remember:'; const LAST_LOGIN_PREFIX = 'ag-last-login:'; function clearLegacyRememberedLogins() { try { Object.keys(localStorage) .filter((key) => key.startsWith(LEGACY_LOGIN_REMEMBER_PREFIX)) .forEach((key) => localStorage.removeItem(key)); } catch { // Armazenamento indisponivel: nao interfere no login. } } clearLegacyRememberedLogins(); function lastLoginKey(slug) { return LAST_LOGIN_PREFIX + String(slug || '').trim().toLowerCase(); } function loadLastLogin(slug) { if (!slug) return ''; try { return String(localStorage.getItem(lastLoginKey(slug)) || '').trim(); } catch { return ''; } } function saveLastLogin(slug, login) { if (!slug || !login) return; try { localStorage.setItem(lastLoginKey(slug), login); } catch { // Armazenamento indisponivel: o acesso continua normalmente. } } function loginFromTenantCredential(slug, value) { const text = String(value || ''); const prefix = String(slug || '').trim().toLowerCase() + '::'; return prefix !== '::' && text.toLowerCase().startsWith(prefix) ? text.slice(prefix.length) : text; } async function storeBrowserCredential(login, pass, companyName) { if (!window.isSecureContext || !navigator.credentials || typeof window.PasswordCredential !== 'function') { return false; } try { const credential = new window.PasswordCredential({ id: login, password: pass, name: companyName || login, }); await navigator.credentials.store(credential); return true; } catch { // O navegador ou uma política corporativa pode bloquear o armazenamento. return false; } } function LoginScreen({ onLogin, notice }) { const slug = window.TENANT_SLUG || ''; const lastLogin = loadLastLogin(slug); const [remember, setRemember] = React.useState(false); const [err, setErr] = React.useState(''); const [busy, setBusy] = React.useState(false); const [empresa, setEmpresa] = React.useState(null); // null = carregando · false = não encontrada const [addr, setAddr] = React.useState(''); React.useEffect(() => { clearLegacyRememberedLogins(); setRemember(false); }, [slug]); React.useEffect(() => { if (!slug) return; window.API.tenantInfo(slug) .then((t) => { setEmpresa(t); document.title = t.name + ' — Agenda'; }) .catch(() => setEmpresa(false)); }, [slug]); const goAddr = (e) => { e.preventDefault(); const s = addr.trim().toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, ''); if (s) window.location.href = (window.BASE_PATH || '/') + s; }; // sem endereço na URL (ou endereço que não existe): pede o endereço da empresa if (!slug || empresa === false) { return (

Agenda

Informe o endereço da sua empresa para entrar

{empresa === false && (
Empresa não encontrada neste endereço. Confira o endereço recebido (ex.: …/suaempresa).
)} setAddr(e.target.value)} placeholder="ex.: delsistemas" autoFocus /> Continuar
Sistema de agendamentos · acesso restrito
); } const submit = async (e) => { e.preventDefault(); if (busy) return; setErr(''); setBusy(true); try { const form = e.currentTarget; const submittedLogin = form.elements.username ? form.elements.username.value : ''; const submittedPass = form.elements.password ? form.elements.password.value : ''; const cleanLogin = loginFromTenantCredential(slug, submittedLogin).trim(); await onLogin(cleanLogin, submittedPass, remember); saveLastLogin(slug, cleanLogin); await storeBrowserCredential(cleanLogin, submittedPass, empresa && empresa.name); } catch (ex) { setErr(ex.message || 'Não foi possível entrar.'); setBusy(false); } }; return (

{empresa ? empresa.name : 'Agenda'}

Entre com seu usuário e senha

{notice && !err && (
{notice}
)} {err && (
{err}
)} {busy ? 'Entrando…' : 'Entrar'}
Sistema de agendamentos · acesso restrito
); } /* ---------------- TELA DE TESTE GRATUITO ---------------- */ function FreeTrialScreen({ onRegister }) { const [name, setName] = React.useState(''); const [businessType, setBusinessType] = React.useState('geral'); const [email, setEmail] = React.useState(''); const [pass, setPass] = React.useState(''); const [err, setErr] = React.useState(''); const [busy, setBusy] = React.useState(false); const [infoOpen, setInfoOpen] = React.useState(false); const [termsOpen, setTermsOpen] = React.useState(false); const [termsAccepted, setTermsAccepted] = React.useState(false); const [privacyOpen, setPrivacyOpen] = React.useState(false); const [privacyAccepted, setPrivacyAccepted] = React.useState(false); const businessOptions = Object.values(window.DB.businessTypes || {}); const termsVersion = '2026-06-30'; const privacyVersion = '2026-07-09'; const features = [ { icon: 'calendar', title: 'Agenda inteligente', text: 'Visualize horários, remarcações, confirmações e histórico em poucos cliques.' }, { icon: 'users', title: 'Clientes organizados', text: 'Cadastre clientes, contatos, observações e vínculos com serviços ou planos.' }, { icon: 'team', title: 'Equipe e permissões', text: 'Controle profissionais, usuários, horários de atendimento e acessos da empresa.' }, { icon: 'wallet', title: 'Financeiro integrado', text: 'Acompanhe receitas, despesas, cobranças e pagamentos dentro do mesmo sistema.' }, ]; const previewSlug = name.trim().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, ''); const submit = async (e) => { e.preventDefault(); if (busy) return; setErr(''); if (!termsAccepted || !privacyAccepted) { setErr('Para criar o teste gratuito, aceite os termos e a política de privacidade do DEL AGENDA.'); return; } setBusy(true); try { const res = await window.API.freeTrial({ name, businessType, email, pass, termsAccepted, termsVersion, privacyAccepted, privacyVersion }); // Redireciona para a nova empresa if (res.slug) { window.location.href = (window.BASE_PATH || '/') + res.slug; } else { setErr(res.message || 'Erro ao realizar cadastro.'); setBusy(false); } } catch (ex) { setErr(ex.message || 'Não foi possível realizar o cadastro.'); setBusy(false); } }; return (
7 dias grátis

Teste Gratuito

Crie sua conta e experimente o DEL AGENDA sem cartão de crédito

setInfoOpen(true)}> Ver o que o sistema oferece {err && (
{err}
)} setName(e.target.value)} placeholder="Minha Clínica" autoFocus /> {previewSlug && (
Seu acesso será: /{previewSlug}
)}
setBusinessType(e.target.value)}> {businessOptions.map((type) => ( ))} setEmail(e.target.value)} placeholder="seu@email.com" autoComplete="username" /> setPass(e.target.value)} placeholder="••••••••" autoComplete="new-password" />
setTermsAccepted(e.target.checked)} />
setPrivacyAccepted(e.target.checked)} />
{busy ? 'Criando Conta…' : 'Criar Conta e Começar Teste'}
Não é necessário cartão de crédito
{infoOpen && ( setInfoOpen(false)} footer={( <> setInfoOpen(false)}>Fechar setInfoOpen(false)}>Começar cadastro )} >
Organize sua operação em um só lugar Durante o teste gratuito você pode conhecer a agenda, cadastrar clientes, configurar serviços, montar equipe e avaliar o financeiro do sistema.
{features.map((feature) => (
{feature.title} {feature.text}
))}
O teste dura 7 dias, não exige cartão de crédito e cria um endereço exclusivo para sua empresa acessar o sistema.
)} {termsOpen && ( setTermsOpen(false)} footer={( <> setTermsOpen(false)}>Fechar { setTermsAccepted(true); setTermsOpen(false); }} > Aceitar termos )} >
Teste gratuito do DEL AGENDA Ao criar a conta, você recebe 7 dias para conhecer os recursos do sistema, sem cobrança automática e sem necessidade de cartão de crédito.
1. Prazo do teste O acesso gratuito dura 7 dias a partir da criação da conta. Depois desse período, o acesso poderá ser limitado até a contratação de um plano.
2. Uso dos dados Os dados informados serão usados para criar a empresa, o usuário administrador, enviar comunicações do sistema e permitir o uso do DEL AGENDA.
3. Responsabilidade do usuário O usuário é responsável pelas informações cadastradas, pelo uso adequado do sistema e pela segurança do e-mail e senha de acesso.
4. Recursos do período gratuito Durante o teste, algumas funções podem ter limites técnicos ou operacionais, especialmente envios automáticos, integrações e recursos pagos.
5. Privacidade e segurança As informações serão tratadas para cadastro, suporte, segurança, comunicação e operação do serviço, conforme a finalidade do teste gratuito.
6. Registro do aceite Ao aceitar estes termos, o sistema registra a data, a versão dos termos e dados técnicos do acesso para comprovar a concordância.
)} {privacyOpen && ( setPrivacyOpen(false)} footer={( <> setPrivacyOpen(false)}>Fechar { setPrivacyAccepted(true); setPrivacyOpen(false); }} > Aceitar política )} >
Como o DEL AGENDA trata dados pessoais Este resumo cobre o cadastro do teste e a operação básica da plataforma. Cada empresa usuária deve complementar o tema com sua própria base legal e comunicação ao titular.
1. Dados coletados Nome da empresa, segmento, e-mail de acesso, senha, IP, navegador e registros técnicos necessários para criar a conta, autenticar acessos e proteger o sistema.
2. Finalidades Cadastro da empresa, autenticação, suporte, comunicações operacionais, segurança, auditoria, prevenção a fraude e funcionamento da agenda.
3. Compartilhamento Os dados podem ser tratados por provedores necessários à operação, como hospedagem, banco de dados, envio de e-mail e integrações de pagamento, sempre na extensão necessária ao serviço.
4. Direitos do titular O titular pode solicitar confirmação de tratamento, acesso, correção, anonimização, eliminação quando cabível e informações sobre compartilhamento, conforme a LGPD.
5. Retenção e segurança Os dados são mantidos pelo tempo necessário ao serviço, obrigações legais, auditoria e defesa. O sistema adota hash de senha, sessões protegidas, trilha de auditoria e rotinas de backup.
6. Papel das partes No modelo SaaS, a empresa usuária normalmente é a controladora dos dados do dia a dia e o DEL AGENDA atua como operador da plataforma.
)}
); } window.FreeTrialScreen = FreeTrialScreen; /* ---------------- MODAL: NOVO/EDITAR USUÁRIO ---------------- */ function UserModal({ user, onSave, onClose, maxUsers = 0, userCount = 0, team = [], protectedAdmin = false }) { const editing = !!user; const [name, setName] = React.useState(user ? user.name : ''); const [login, setLogin] = React.useState(user ? user.login : ''); const [pass, setPass] = React.useState(''); const [role, setRole] = React.useState(user ? user.role : 'user'); const linkedPro = user && user.proId ? team.find((p) => Number(p.id) === Number(user.proId)) : null; const [color, setColor] = React.useState(user ? (user.color || (linkedPro && linkedPro.color) || '') : ''); const [busy, setBusy] = React.useState(false); const usedColors = (team || []).filter((p) => !p.archived && (!user || Number(p.id) !== Number(user.proId))).map((p) => p.color); const userLimitReached = maxUsers > 0 && userCount >= maxUsers; const blockedByUserLimit = !editing && userLimitReached; const save = async () => { if (busy || blockedByUserLimit) return; setBusy(true); try { const payload = editing ? { name, role, color, ...(pass ? { pass } : {}) } : { name, login, pass, role, color }; await onSave(payload); onClose(); } catch (ex) { window.alert(ex.message || 'Erro ao salvar usuário.'); setBusy(false); } }; return ( Cancelar {busy ? 'Salvando…' : 'Salvar'} )} >
setName(e.target.value)} placeholder="Nome da pessoa" /> setLogin(e.target.value)} placeholder="ex.: recepcao, ana.silva" /> setPass(e.target.value)} placeholder={editing ? 'deixe em branco p/ manter' : 'mínimo 8 caracteres'} autoComplete="new-password" /> setRole(e.target.value)}> {blockedByUserLimit && (
Limite de usuários com login atingido ({maxUsers}). Faça upgrade para criar outro acesso.
)}
); } /* ---------------- TELA: USUÁRIOS ---------------- */ function UsersScreen(ctx) { const [users, setUsers] = React.useState(null); const [err, setErr] = React.useState(''); const [modal, setModal] = React.useState(null); // {} novo | {user} editar | null const load = React.useCallback(async () => { try { const r = await window.API.users.list(); setUsers(r.users); } catch (ex) { setErr(ex.message || 'Erro ao carregar usuários.'); } }, []); React.useEffect(() => { load(); }, [load]); const roleLabel = (r) => (r === 'admin' ? 'Administrador' : (r === 'pro' ? 'Profissional' : 'Usuário')); const save = async (payload, editId) => { const r = editId ? await window.API.users.update(editId, payload) : await window.API.users.create(payload); setUsers((list) => { if (!list) return list; return list.some((u) => u.id === r.user.id) ? list.map((u) => (u.id === r.user.id ? r.user : u)) : [...list, r.user]; }); if (ctx.user && r.user && ctx.user.id === r.user.id && ctx.updateCurrentUser) { ctx.updateCurrentUser(r.user); } ctx.toast(editId ? 'Usuário atualizado' : 'Usuário criado'); }; const toggleStatus = async (u) => { try { const next = u.status === 'ativo' ? 'suspenso' : 'ativo'; const r = await window.API.users.update(u.id, { status: next }); setUsers((list) => list.map((x) => (x.id === u.id ? r.user : x))); ctx.toast(next === 'ativo' ? 'Usuário reativado' : 'Usuário suspenso'); } catch (ex) { ctx.toast(ex.message || 'Erro', 'x'); } }; const remove = async (u) => { if (!window.confirm(`Excluir o usuário "${u.name}"? Esta ação não pode ser desfeita.`)) return; try { await window.API.users.remove(u.id); setUsers((list) => list.filter((x) => x.id !== u.id)); ctx.toast('Usuário excluído'); } catch (ex) { ctx.toast(ex.message || 'Erro', 'x'); } }; const maxUsers = (ctx.limits && ctx.limits.maxUsers) || 0; const userCount = (users || []).length; const primaryAdminId = (users || []).filter((u) => u.role === 'admin').reduce((lowest, u) => lowest === null || u.id < lowest ? u.id : lowest, null); const currentIsPrimaryAdmin = !!(ctx.user && ctx.user.id === primaryAdminId); const userLimitReached = maxUsers > 0 && userCount >= maxUsers; return (
setModal({})}>Novo usuário} /> {err &&
{err}
} {userLimitReached && (
Limite de usuários com login atingido ({maxUsers}). Contas de administrador, usuário e profissional usam essa mesma cota. Membros da equipe sem login não entram na contagem.
)}
{users === null && } {users && users.length === 0 && } {users && users.map((u) => { const isMe = ctx.user && u.id === ctx.user.id; const isPrimaryAdmin = u.id === primaryAdminId; const canManageTarget = u.role !== 'admin' || currentIsPrimaryAdmin; return ( ); })}
NomeUsuárioPapelSituação
Carregando…
Nenhum usuário.
{u.name || '—'}{isMe && (você)}
{u.login} {roleLabel(u.role)} {u.status === 'ativo' ? 'Ativo' : 'Suspenso'} e.stopPropagation()}>
{(!isMe || currentIsPrimaryAdmin) && canManageTarget && ( }> {!isPrimaryAdmin && <>
} )}
{modal && ( setModal(null)} onSave={(payload) => save(payload, modal.user ? modal.user.id : null)} /> )}
); } Object.assign(window, { LoginScreen, UsersScreen, UserModal });