const { useState, useEffect } = React;
// --- Funcões Auxilares e Máscaras ---
function validarCPF(cpf) {
cpf = cpf.replace(/[^\d]+/g, '');
if (cpf.length !== 11 || !!cpf.match(/(\d)\1{10}/)) return false;
let soma = 0, resto;
for (let i = 1; i <= 9; i++) soma += parseInt(cpf.substring(i - 1, i)) * (11 - i);
resto = (soma * 10) % 11;
if (resto === 10 || resto === 11) resto = 0;
if (resto !== parseInt(cpf.substring(9, 10))) return false;
soma = 0;
for (let i = 1; i <= 10; i++) soma += parseInt(cpf.substring(i - 1, i)) * (12 - i);
resto = (soma * 10) % 11;
if (resto === 10 || resto === 11) resto = 0;
if (resto !== parseInt(cpf.substring(10, 11))) return false;
return true;
}
function formatCPF(value) {
if (!value) return '';
return value.replace(/\D/g, '')
.replace(/(\d{3})(\d)/, '$1.$2')
.replace(/(\d{3})(\d)/, '$1.$2')
.replace(/(\d{3})(\d{1,2})/, '$1-$2')
.replace(/(-\d{2})\d+?$/, '$1');
}
function formatCNPJ(value) {
if (!value) return '';
return value.replace(/\D/g, '')
.replace(/(\d{2})(\d)/, '$1.$2')
.replace(/(\d{3})(\d)/, '$1.$2')
.replace(/(\d{3})(\d)/, '$1/$2')
.replace(/(\d{4})(\d{1,2})/, '$1-$2')
.replace(/(-\d{2})\d+?$/, '$1');
}
function formatCardNumber(value) {
return value.replace(/\D/g, '').replace(/(\d{4})/g, '$1 ').trim().substring(0, 19);
}
function formatExpiry(value) {
const v = value.replace(/\D/g, '');
if (v.length >= 2) return v.substring(0, 2) + '/' + v.substring(2, 4);
return v;
}
function validarSenha(senha) {
const minLength = 8;
const hasUpperCase = /[A-Z]/.test(senha);
const hasNumber = /[0-9]/.test(senha);
const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(senha);
if (senha.length < minLength) return "A senha deve ter pelo menos 8 caracteres.";
if (!hasUpperCase) return "A senha deve conter pelo menos uma letra maiúscula.";
if (!hasNumber) return "A senha deve conter pelo menos um número.";
if (!hasSpecial) return "A senha deve conter pelo menos um caractere especial.";
return null;
}
// RemovidonotasDB mock e integrado com MariaDB via backend.
const API_URL = 'https://api.iaagro.app/api';
// Interceptor global de requisições para anexar o JWT Token
const originalFetch = window.fetch;
window.fetch = async function () {
let [resource, config] = arguments;
if (typeof resource === 'string' && resource.startsWith(API_URL)) {
const userStr = localStorage.getItem('iaagro_user');
if (userStr) {
try {
const user = JSON.parse(userStr);
if (user && user.token) {
if (!config) config = {};
if (!config.headers) config.headers = {};
// URLs de autenticação públicas não exigem token
const isAuthRoute = resource.includes('/login') ||
resource.includes('/register') ||
resource.includes('/forgot') ||
resource.includes('/reset');
if (!isAuthRoute) {
config.headers['Authorization'] = 'Bearer ' + user.token;
}
}
} catch (e) {
console.error("Erro ao ler token", e);
}
}
}
return originalFetch(resource, config);
};
function App() {
const [view, setView] = useState(() => {
const params = new URLSearchParams(window.location.search);
if (window.location.hostname === 'portal.iaagro.app') {
return params.get('view') || 'login';
}
return params.get('view') || 'landing';
});
const handleSetView = (newView) => {
const isPortalView = ['login', 'register', 'termos', 'portal', 'checkout-success', 'checkout-cancel'].includes(newView);
// Se está no domínio principal e quer acessar o portal
if (window.location.hostname === 'iaagro.app' && isPortalView) {
window.location.href = `https://portal.iaagro.app/?view=${newView}`;
return;
}
// Se está no portal e quer voltar ao site
if (window.location.hostname === 'portal.iaagro.app' && newView === 'landing') {
window.location.href = 'https://iaagro.app/';
return;
}
setView(newView);
};
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [notification, setNotification] = useState({ show: false, msg: '', type: 'info' });
const notify = (msg, type = 'info') => {
setNotification({ show: true, msg, type });
setTimeout(() => setNotification({ show: false, msg: '', type: 'info' }), 4000);
};
// --- Sistema de Temas ---
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const applyTheme = (isDark) => {
const newTheme = isDark ? 'dark' : 'light';
setTheme(newTheme);
document.documentElement.setAttribute('data-theme', newTheme);
};
applyTheme(mediaQuery.matches);
const handleChange = (e) => applyTheme(e.matches);
mediaQuery.addEventListener('change', handleChange);
// Fade-out global loader after React mounts
const loader = document.getElementById('global-loader');
if (loader) {
document.body.style.overflow = 'hidden';
setTimeout(() => {
loader.style.opacity = '0';
loader.style.visibility = 'hidden';
document.body.style.overflow = '';
setTimeout(() => loader.remove(), 500);
}, 300); // Dar um tempo extra para imagens e vídeos começarem a carregar
}
// Persistência de Login: Recuperar do localStorage
const savedUser = localStorage.getItem('iaagro_user');
if (savedUser) {
try {
const parsedUser = JSON.parse(savedUser);
// 🔥 VALIDAÇÃO REAL NO BACKEND
fetch(`${API_URL}/user/${parsedUser.id}`)
.then(res => res.json())
.then(data => {
if (!data || data.error) {
localStorage.removeItem('iaagro_user');
setUser(null);
handleSetView(window.location.hostname === 'portal.iaagro.app' ? 'login' : 'landing');
} else {
setUser(data);
handleSetView('portal');
}
})
.catch(() => {
handleSetView(window.location.hostname === 'portal.iaagro.app' ? 'login' : 'landing');
});
} catch (e) {
localStorage.removeItem('iaagro_user');
}
}
return () => mediaQuery.removeEventListener('change', handleChange);
}, []);
// --- Detectar retorno do Stripe Checkout via query params ---
useEffect(() => {
const params = new URLSearchParams(window.location.search);
if (params.get("checkout") === "success") {
handleSetView('checkout-success');
window.history.replaceState({}, document.title, window.location.pathname);
} else if (params.get("checkout") === "cancel") {
handleSetView('checkout-cancel');
window.history.replaceState({}, document.title, window.location.pathname);
}
}, []);
// --- Observador para Animações de Scroll (Reveal) ---
useEffect(() => {
const observerOptions = {
threshold: 0.15,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('active');
// Opcional: parar de observar após animar uma vez
// observer.unobserve(entry.target);
}
});
}, observerOptions);
const revealElements = document.querySelectorAll('.reveal');
revealElements.forEach(el => observer.observe(el));
// Re-observar se a view mudar (importante p/ SPA)
return () => {
revealElements.forEach(el => observer.unobserve(el));
};
}, [view]); // Dispara novamente ao trocar de tela (landing -> portal etc)
const switchTheme = () => {
const newTheme = theme === 'dark' ? 'light' : 'dark';
setTheme(newTheme);
document.documentElement.setAttribute('data-theme', newTheme);
};
const handleLogin = (userData) => {
localStorage.setItem('iaagro_user', JSON.stringify(userData));
setUser(userData);
handleSetView('portal');
};
const handleLogout = () => {
localStorage.removeItem('iaagro_user');
setUser(null);
handleSetView('landing');
};
return (
{/* Botão Global de Tema - Efeito Glass Fixo (Apenas em outras telas, no landing vai p/ navbar) */}
{view !== 'landing' && (
{theme === 'dark' ? : }
)}
{view === 'landing' && }
{view === 'login' && }
{view === 'register' && }
{view === 'termos' && }
{view === 'portal' && }
{view === 'checkout-success' && }
{view === 'checkout-cancel' && }
{/* Botão de WhatsApp Flutuante */}
{/* Sistema de Notificação Customizado */}
{notification.msg}
);
}
// === LANDING PAGE ===
function Landing({ onNavigate, theme, onSwitchTheme }) {
const logoSrc = theme === 'dark' ? 'assets/logo-dark.svg' : 'assets/logo-light.svg';
const [menuOpen, setMenuOpen] = React.useState(false);
const [activeDots, setActiveDots] = React.useState(1); // Foco no meio (index 1)
const [billingCycle, setBillingCycle] = React.useState('annual'); // 'monthly' | 'annual'
const plansRef = React.useRef(null);
const [isVideoMuted, setIsVideoMuted] = React.useState(true);
const [isVideoPlaying, setIsVideoPlaying] = React.useState(true);
const videoRef = React.useRef(null);
const toggleVideoMute = () => {
if (videoRef.current) {
videoRef.current.muted = !isVideoMuted;
setIsVideoMuted(!isVideoMuted);
}
};
const toggleVideoPlay = () => {
if (videoRef.current) {
if (isVideoPlaying) {
videoRef.current.pause();
} else {
videoRef.current.play();
}
setIsVideoPlaying(!isVideoPlaying);
}
};
React.useEffect(() => {
// Focar no plano Família (meio) no mobile
if (window.innerWidth <= 768 && plansRef.current) {
setTimeout(() => {
if (plansRef.current && plansRef.current.children[1]) {
const middleElement = plansRef.current.children[1];
const container = plansRef.current;
container.scrollLeft = middleElement.offsetLeft - container.offsetLeft - (container.clientWidth / 2) + (middleElement.clientWidth / 2);
}
}, 100);
}
}, [billingCycle]);
const handleScroll = (e) => {
if (window.innerWidth > 768) return;
const scrollLeft = e.target.scrollLeft;
const scrollWidth = e.target.scrollWidth - e.target.clientWidth;
const percentage = scrollLeft / scrollWidth;
let index = 1;
if (percentage < 0.25) index = 0;
else if (percentage > 0.75) index = 2;
if (index !== activeDots) setActiveDots(index);
};
return (
{/* Esquerda: Tema no mobile, Logo no desktop */}
{/* Centro: Logo no mobile, Links no desktop */}
{/* Direita: Acoes/Hamburger */}
onNavigate('login')}>Entrar
onNavigate('register')}>Registre-se
{theme === 'dark' ? : }
setMenuOpen(!menuOpen)}>
Emita suas Notas Fiscais peloWhatsApp em Segundos
Sem burocracia, sem computadores difíceis. Mande um áudio ou texto e a nossa inteligência faz todo o trabalho chato por você.
onNavigate('register')}>
Criar Conta Grátis
document.getElementById('features').scrollIntoView()}>
Entender Como Funciona
{/* RECURSOS */}
Como a IAAGRO trabalha?
Um fluxo completo e automatizado, do campo até a Receita Federal.
1. Você usa o WhatsApp?
É só isso que você precisa. Nada de instalar programas ou aprender sistemas novos.
2. Envie um áudio, texto ou foto
Fale ou escreva o que vendeu, a quantidade e o comprador. Nossa inteligência entende tudo.
3. Sua nota pronta na hora
A nota é gerada automaticamente e enviada em PDF direto no seu WhatsApp, pronta para usar.
4. Tudo salvo e seguro
Suas notas ficam guardadas em segurança enquanto você for nosso cliente. Seu contador pode baixar tudo depois.
Escolha seu Plano
Flexibilidade para todos os tamanhos de negócios e produtores rurais.
setBillingCycle('monthly')}
>
Mensal
setBillingCycle('annual')}
>
Anual
Produtor
R$ {billingCycle === 'monthly' ? '74' : '64'},90
{billingCycle === 'monthly' ? 'mensal' : 'mensal (faturado anualmente)'}
{billingCycle === 'annual' && 1 Certificado A1 Grátis }
30 Emissões mensais
1 CNPJ/CPF
Via WhatsApp (texto)
3 números autorizados
Suporte em 24h úteis
onNavigate('register')}>Começar
Mais Escolhido
Família
R$ {billingCycle === 'monthly' ? '104' : '94'},90
{billingCycle === 'monthly' ? 'mensal' : 'mensal (faturado anualmente)'}
{billingCycle === 'annual' && 1 Certificado A1 Grátis }
100 Emissões mensais
3 CNPJs/CPFs
Via WhatsApp (texto, áudio, img)
3 Acessos ao portal / 5 WhatsApps
Suporte em 12h úteis
onNavigate('register')}>Assinar Família
Cooperativa
R$ {billingCycle === 'monthly' ? '174' : '154'},90
{billingCycle === 'monthly' ? 'mensal' : 'mensal (faturado anualmente)'}
{billingCycle === 'annual' && 1 Certificado A1 Grátis }
500 Emissões mensais
CNPJ/CPF Ilimitado
5 Acessos ao portal / 10 WhatsApps
Relatórios completos por produtor
Suporte em 4h úteis
window.open('https://wa.me/551530422070', '_blank')}>Falar com Vendas
{/* Paginação do Carrossel (Mobile Only) */}
Addons (Adicionais)
Potencialize seu plano com recursos extras sob demanda
Relatório Mensal Email/Wpp R$ 14,90
Buscador de NF-e de Compras (Produtor) R$ 14,90
Relatório Compras/Vendas (IR) R$ 14,90
Autorização de Número Adicional (Emissão) R$ 4,90
{/* DEPOIMENTOS */}
O que dizem os produtores
Opiniões reais de quem já eliminou processos ultrapassados no agronegócio.
CA
Carlos Almeida Fazenda Boa Esperança
“Antigamente eu perdia horas no computador puxando XML, era horrível estar no pasto e ter que voltar. Hoje eu mando um texto rápido pelo WhatsApp com os dados da carga, e a nota desponta no e-mail do cliente na mesma hora. Fantástico.”
MF
Marta Fidelis Cooperativa Agrosul
“Conseguimos alocar a IAAGRO para operar notas de mais de 12 caminhões por dia. Basta digitar o pedido e a IA faz a conversão tributária exata, zeramos multas por esquecimento de regras estaduais complexas.”
RR
Rafael Rios Engenheiro Agrônomo Independente
“Poder gerar a NF-e pelo WhatsApp é um divisor de águas. O sistema reconhece a descrição dos produtos e fecha a nota em segundos, sem precisar abrir sistemas complexos. Sensacional, recomendo!”
{/* FAQ E DUVIDAS */}
{/* FOOTER OFICIAL NUUV*/}
);
}
// === TERMOS ===
function Termos({ onNavigate, theme }) {
const logoSrc = theme === 'dark' ? 'assets/logo-dark.svg' : 'assets/logo-light.svg';
return (
onNavigate('register')} className="btn btn-outline" style={{ padding: '0.6rem 1.2rem', borderRadius: '8px' }}>
Voltar
Termos de Uso e Condições
Ao utilizar os serviços da IAAGRO, você concorda com os seguintes termos e condições:
1. Responsabilidade pelos Dados: A IAAGRO atua exclusivamente como uma ferramenta tecnológica facilitadora para a emissão de Notas Fiscais Eletrônicas (NF-e). NÃO NOS RESPONSABILIZAMOS pelos dados fiscais, valores, alíquotas, NCMs ou quaisquer outras informações tributárias informadas ou sugeridas pelo sistema.
2. Sugestões da Inteligência Artificial: As sugestões de códigos e tributações geradas por nossa Inteligência Artificial são baseadas em padrões de mercado, mas não substituem a validação de um contador qualificado. O cliente deve SEMPRE se atentar e conferir os dados antes de confirmar a emissão, pois apenas emitimos e damos sugestões.
3. Obrigação do Usuário: É de inteira responsabilidade do produtor rural ou de sua contabilidade garantir que todas as transações fiscais estejam de acordo com a legislação vigente em seu estado.
4. Armazenamento: Manteremos seus arquivos XML e PDF armazenados de forma segura em nossos servidores pelo período em que você mantiver uma assinatura ativa conosco.
onNavigate('register')}>Li e Aceito
);
}
// === LOGIN ===
function Login({ onNavigate, onLogin, theme, notify }) {
const logoSrc = theme === 'dark' ? 'assets/logo-dark.svg' : 'assets/logo-light.svg';
const [phone, setPhone] = useState('');
const [password, setPassword] = useState('');
const [showPass, setShowPass] = useState(false);
// Forgot password state
const [forgotStep, setForgotStep] = useState(0); // 0=login, 1=enter phone, 2=enter code+newPass
const [forgotPhone, setForgotPhone] = useState('');
const [resetCode, setResetCode] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmNewPassword, setConfirmNewPassword] = useState('');
const [forgotLoading, setForgotLoading] = useState(false);
const [showNewPass, setShowNewPass] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
if (!phone || !password) return notify("Preencha todos os campos.", "error");
try {
const response = await fetch(`${API_URL}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone: phone.replace(/\D/g, ''), password })
});
const data = await response.json();
if (response.ok) {
onLogin(data);
notify("Bem-vindo ao IAAGRO!", "success");
} else {
notify(data.error || "Erro ao fazer login.", "error");
}
} catch (err) {
notify("Erro ao conectar com o servidor.", "error");
}
};
const handleForgotSendCode = async (e) => {
e.preventDefault();
if (!forgotPhone) return notify("Informe seu telefone.", "error");
setForgotLoading(true);
try {
const res = await fetch(`${API_URL}/forgot-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone: forgotPhone.replace(/\D/g, '') })
});
const data = await res.json();
if (res.status === 429) {
notify(data.error || "Muitas tentativas. Aguarde.", "error");
} else if (res.ok) {
notify("Código enviado via WhatsApp! Verifique seu celular.", "success");
setForgotStep(2);
} else {
notify(data.error || "Erro ao enviar código.", "error");
}
} catch {
notify("Erro ao conectar com o servidor.", "error");
}
setForgotLoading(false);
};
const handleResetPassword = async (e) => {
e.preventDefault();
if (!resetCode || !newPassword) return notify("Preencha todos os campos.", "error");
const erroSenha = validarSenha(newPassword);
if (erroSenha) return notify(erroSenha, "error");
if (newPassword !== confirmNewPassword) return notify("As senhas não coincidem.", "error");
setForgotLoading(true);
try {
const res = await fetch(`${API_URL}/reset-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
phone: forgotPhone.replace(/\D/g, ''),
code: resetCode,
newPassword
})
});
const data = await res.json();
if (res.ok) {
notify("Senha redefinida com sucesso! Faça login.", "success");
setForgotStep(0);
setResetCode('');
setNewPassword('');
setConfirmNewPassword('');
} else {
notify(data.error || "Erro ao redefinir senha.", "error");
}
} catch {
notify("Erro ao conectar com o servidor.", "error");
}
setForgotLoading(false);
};
// --- Forgot password step 1: enter phone ---
if (forgotStep === 1) {
return (
setForgotStep(0)} className="btn btn-outline" style={{ padding: '0.6rem 1.2rem', borderRadius: '8px' }}>
Voltar ao Login
Recuperar Senha
Informe seu telefone cadastrado para receber o código via WhatsApp
);
}
// --- Forgot password step 2: enter code + new password ---
if (forgotStep === 2) {
return (
setForgotStep(1)} className="btn btn-outline" style={{ padding: '0.6rem 1.2rem', borderRadius: '8px' }}>
Reenviar código
Nova Senha
Digite o código de 6 dígitos enviado via WhatsApp e sua nova senha
Reenviar código
);
}
// --- Normal login ---
return (
onNavigate('landing')} className="btn btn-outline" style={{ padding: '0.6rem 1.2rem', borderRadius: '8px' }}>
Voltar p/ o Início
Acessar Portal
Emita suas notas via IA no WhatsApp
Ainda não é cliente? onNavigate('register')}>Cadastre-se
{ setForgotPhone(phone); setForgotStep(1); }}>Esqueci minha senha
);
}
// === REGISTER ===
function Register({ onNavigate, onLogin, theme, notify }) {
const logoSrc = theme === 'dark' ? 'assets/logo-dark.svg' : 'assets/logo-light.svg';
const [formData, setFormData] = useState({ name: '', cpf: '', email: '', cnpj: '', phone: '', password: '', confirmPassword: '' });
const [showPass, setShowPass] = useState(false);
const [termsAccepted, setTermsAccepted] = useState(false);
const handleChange = (e) => {
setFormData({ ...formData, [e.target.id]: e.target.value });
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!formData.name || !formData.email || !formData.cnpj || !formData.phone || !formData.password || !formData.cpf) {
return notify("Preencha todos os campos.", "error");
}
if (!termsAccepted) {
return notify("Você deve aceitar os Termos de Uso e Condições.", "error");
}
if (!validarCPF(formData.cpf)) {
return notify("CPF Inválido. Digite um CPF correto.", "error");
}
const erroSenha = validarSenha(formData.password);
if (erroSenha) return notify(erroSenha, "error");
if (formData.password !== formData.confirmPassword) {
return notify("As senhas não coincidem.", "error");
}
try {
const response = await fetch(`${API_URL}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...formData,
cpf: formData.cpf.replace(/\D/g, ''),
phone: formData.phone.replace(/\D/g, '')
})
});
const data = await response.json();
if (response.ok) {
onLogin(data);
notify("Cadastro realizado com sucesso!", "success");
} else {
notify(data.error || "Erro ao registrar.", "error");
}
} catch (err) {
notify("Erro ao conectar com o servidor.", "error");
}
};
return (
onNavigate('landing')} className="btn btn-outline" style={{ padding: '0.6rem 1.2rem', borderRadius: '8px' }}>
Voltar p/ o Início
Começar Grátis
Prepare sua conta e seu certificado A1
Já possui conta? onNavigate('login')}>Fazer Login
);
}
// === PORTAL DASHBOARD ===
function Portal({ user, onLogout, theme, notify }) {
const logoSrc = theme === 'dark' ? 'assets/logo-dark.svg' : 'assets/logo-light.svg';
const [tab, setTab] = useState('perfil');
const [activeCnpj, setActiveCnpj] = useState(() => {
if (user.available_cnpjs && user.available_cnpjs.length > 0) {
return user.available_cnpjs[0];
}
return { type: 'main', cnpj: user.cnpj, name: user.name, id: user.id };
});
return (
{/* Sidebar */}
setTab('notas')}>
Minhas Notas
setTab('plano')}>
Meu Plano
setTab('autorizados')}>
Números Autorizados
{user.name.charAt(0).toUpperCase()}
{user.name.split(' ')[0]}
setTab('perfil')} style={{ color: tab === 'perfil' ? 'var(--primary-color)' : 'inherit', fontSize: '0.9rem' }}>
Produtor Autorizado
Sair do Portal
{!user.subscription_valid && (
Atenção: Sua assinatura está vencida.
setTab('plano')}>Renovar / Trocar Pagamento
)}
Empresa Ativa (CNPJ)
setActiveCnpj(user.available_cnpjs.find(c => c.cnpj === e.target.value))}
style={{ padding: '0.6rem 1rem', borderRadius: '8px', border: '1px solid var(--primary-color)', background: 'var(--bg-color)', color: 'var(--text-primary)', width: '350px', fontSize: '1rem', fontWeight: 'bold' }}
>
{user.available_cnpjs?.map(c => (
{c.name} ({formatCNPJ(c.cnpj)})
))}
{activeCnpj.hasCertificate && (
Certificado Ativo {activeCnpj.certificateExpirationDate ? `(Vence em: ${new Date(activeCnpj.certificateExpirationDate).toLocaleDateString('pt-BR')})` : ''}
)}
setTab('nova-empresa')}> Adicionar CNPJ
{tab === 'notas' && }
{tab === 'perfil' && }
{tab === 'plano' && }
{tab === 'autorizados' && }
{tab === 'nova-empresa' && window.location.reload()} />}
);
}
function NovaEmpresaView({ user, notify, onAdded }) {
const [cnpj, setCnpj] = useState('');
const [name, setName] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
if (!cnpj || !name) return notify("Preencha CNPJ e Nome", "error");
setLoading(true);
try {
const userStr = localStorage.getItem('iaagro_user');
const token = userStr ? JSON.parse(userStr).token : null;
const res = await fetch(`${API_URL}/user/empresas`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({ cnpj: cnpj.replace(/\D/g, ''), name })
});
const data = await res.json();
if (res.ok) {
notify("Empresa adicionada com sucesso!", "success");
setTimeout(() => onAdded(), 1000);
} else {
notify(data.error || "Erro ao adicionar empresa", "error");
}
} catch(err) {
notify("Erro de conexão", "error");
}
setLoading(false);
};
return (
Adicionar Novo CNPJ
Insira os dados da nova empresa. O limite de CNPJs depende do seu plano atual.
);
}
function NotasView({ user, notify }) {
const userId = user.id;
const [notas, setNotas] = useState([]);
const [search, setSearch] = useState('');
const [dataInicio, setDataInicio] = useState('');
const [dataFim, setDataFim] = useState('');
const [selected, setSelected] = useState([]);
useEffect(() => {
const fetchNotas = async () => {
try {
const response = await fetch(`${API_URL}/notas?userId=${userId}`);
const data = await response.json();
if (response.ok && Array.isArray(data)) {
setNotas(data);
} else if (data.error) {
console.error(data.error);
}
} catch (err) {
console.error("Erro ao carregar notas.");
}
};
if (userId) fetchNotas();
}, [userId]);
// Filtragem
const filteredNotas = notas.filter(n => {
let match = true;
if (search && !n.numero.includes(search) && !n.chave.includes(search)) match = false;
if (dataInicio && n.date < dataInicio) match = false;
if (dataFim && n.date > dataFim) match = false;
return match;
});
const toggleSelectAll = (e) => {
if (e.target.checked) setSelected(filteredNotas.map(n => n.id));
else setSelected([]);
};
const toggleSelect = (id) => {
if (selected.includes(id)) setSelected(selected.filter(s => s !== id));
else setSelected([...selected, id]);
};
const handleDownload = async (nota, type) => {
const key = type === 'xml' ? nota.xml_key : nota.pdf_key;
if (!key) return notify("Arquivo não disponível.", "error");
try {
notify("Iniciando download...", "info");
const url = `${API_URL}/download?key=${encodeURIComponent(key)}`;
const token = localStorage.getItem('iaagro_token');
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) throw new Error("Falha no download");
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = downloadUrl;
a.download = `nota_${nota.numero}_${type}.${type}`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(downloadUrl);
document.body.removeChild(a);
notify("Download concluído!", "success");
} catch (err) {
console.error(err);
notify("Erro ao baixar o arquivo. Tente novamente.", "error");
}
};
const handleBulkDownload = (type) => {
if (selected.length === 0) return notify("Selecione ao menos uma nota.", "error");
notify(`Preparando arquivo ZIP com ${selected.length} notas (${type.toUpperCase()})`, "success");
}
const handleAcao = async (rota, body, msgSucesso) => {
try {
const userStr = localStorage.getItem('iaagro_user');
const token = userStr ? JSON.parse(userStr).token : null;
notify("Processando requisição no ACBr...", "info");
const res = await fetch(`${API_URL}/notas/${rota}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify(body)
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Erro na API do ACBr");
notify(msgSucesso, "success");
} catch (err) {
console.error(err);
notify(err.message, "error");
}
};
const handleCancelar = (n) => {
if(!n.chave) return notify("A nota não possui chave para ser cancelada.", "error");
const just = prompt("Informe a justificativa para cancelar (mínimo 15 caracteres):");
if (!just || just.length < 15) return notify("Justificativa inválida", "error");
handleAcao('cancelar', { chave: n.chave, justificativa: just }, "Cancelamento processado no ACBr!");
};
const handleCce = (n) => {
if(!n.chave) return notify("A nota não possui chave para carta de correção.", "error");
const corr = prompt("Informe o texto da Carta de Correção (mínimo 15 caracteres):");
if (!corr || corr.length < 15) return notify("Texto de correção inválido", "error");
handleAcao('cce', { chave: n.chave, correcao: corr }, "Carta de Correção processada no ACBr!");
};
const handleInutilizar = (n) => {
const just = prompt("Justificativa para inutilizar numeração (mínimo 15 caracteres):");
if (!just || just.length < 15) return notify("Justificativa inválida", "error");
const num = prompt("Informe o número exato para inutilizar (ex: 100):");
if (!num) return;
const ano = n.date ? new Date(n.date).getFullYear().toString().slice(-2) : new Date().getFullYear().toString().slice(-2);
handleAcao('inutilizar', {
ano, modelo: "55", serie: n.serie || "1", numeroInicial: num, numeroFinal: num, justificativa: just
}, "Inutilização processada no ACBr!");
};
const renderStatus = (nota) => {
const temEvento = nota.status === 'Cancelada' || nota.status === 'Inutilizada' || (nota.eventos && nota.eventos.length > 0);
if (temEvento) {
return (
Aviso na Nota
{nota.eventos && nota.eventos.length > 0 ? nota.eventos.join(', ') : nota.status}
);
}
return ;
}
return (
Notas Emitidas e Processadas
Histórico completo de transmissões via Secretaria da Fazenda.
Cota: {notas.length} / {user.notes_limit > 1000 ? 'Ilimitado' : (user.notes_limit || 0)} notas
{filteredNotas.length > 0 ? (
) : (
Nenhuma nota encontrada nos filtros.
Seus apontamentos com IA ainda não geraram uma Sefaz oficial ou os filtros estão vazios.
)}
)
}
function PerfilView({ user, notify, activeCnpj }) {
const targetCnpj = activeCnpj ? activeCnpj.cnpj : user.cnpj;
const [cnpjData, setCnpjData] = useState(null);
const [fetchingCnpj, setFetchingCnpj] = useState(false);
const [codMunicipio, setCodMunicipio] = useState(null);
const [fetchingCodMunicipio, setFetchingCodMunicipio] = useState(false);
const [serieNfe, setSerieNfe] = useState(user.serie_nfe || '7');
const [ultimaNota, setUltimaNota] = useState(user.ultima_nota || 1);
const [fileError, setFileError] = useState('');
const [isDrag, setIsDrag] = useState(false);
const [showModal, setShowModal] = useState(false);
const [correctionField, setCorrectionField] = useState('Razão Social');
const [correctionData, setCorrectionData] = useState('');
const [dropdownOpen, setDropdownOpen] = useState(false);
const [certFile, setCertFile] = useState(null);
const [certPass, setCertPass] = useState('');
const [saving, setSaving] = useState(false);
const modalOptions = ["Razão Social", "Endereço / Logradouro", "Inscrição Estadual", "Natureza Jurídica", "Código IBGE"];
useEffect(() => {
if (targetCnpj) {
setFetchingCodMunicipio(true);
const cleanCnpj = targetCnpj.replace(/\D/g, '');
fetch(`https://brasilapi.com.br/api/cnpj/v1/${cleanCnpj}`)
.then(res => res.json())
.then(data => {
if (data.razao_social) setCodMunicipio(data);
})
.catch(err => console.log('Erro ao buscar código do município'))
.finally(() => setFetchingCodMunicipio(false));
fetch(`https://api.opencnpj.org/${cleanCnpj}`)
.then(res => res.json())
.then(data => {
if (data.razao_social) setCnpjData(data);
})
.catch(err => console.log('Erro ao buscar CNPJ (Usando Mock Local)'))
.finally(() => setFetchingCnpj(false));
}
}, [targetCnpj]);
const processFile = (file) => {
setFileError('');
if (file) {
if (!file.name.toLowerCase().endsWith('.pfx')) {
setFileError('Segurança: Arquivo rejeitado. Por favor, envie o certificado A1 original criptografado em .PFX.');
} else {
setFileError('Sucesso: ' + file.name + ' anexado pronto para validação de senha.');
setCertFile(file);
console.log("Certificado PFX aceito na checagem front-end", file);
}
}
};
const handleDrag = (e) => {
e.preventDefault(); e.stopPropagation();
if (e.type === 'dragenter' || e.type === 'dragover') setIsDrag(true);
else setIsDrag(false);
};
const handleDrop = (e) => {
e.preventDefault(); e.stopPropagation();
setIsDrag(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
processFile(e.dataTransfer.files[0]);
}
};
const handleSaveNfeConfig = async () => {
try {
const userStr = localStorage.getItem('iaagro_user');
const token = userStr ? JSON.parse(userStr).token : null;
notify("Salvando configurações...", "info");
const res = await fetch(`${API_URL}/user/update-nfe-config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({ serie_nfe: serieNfe, ultima_nota: ultimaNota })
});
if (!res.ok) throw new Error("Erro ao salvar configuração.");
notify("Configuração de NF-e salva com sucesso!", "success");
} catch (err) {
notify(err.message, "error");
}
};
return (
Meu Perfil Físcal
Mantenha seus dados e certificados atualizados para o motor de Inteligência Artificial processar sem falhas.
{fetchingCnpj &&
Sincronizando metadados...
}
{cnpjData && !fetchingCnpj && (
Dados da Empresa Sincronizados Oficialmente!
Dados retonados e validados do Emitente {cnpjData.razao_social}
{ e.preventDefault(); setShowModal(true); }}>
Algum dado incorreto?
)}
{showModal && (
Solicitar Correção de Dado
Qual dado oficial está retornando incorreto?
setDropdownOpen(!dropdownOpen)} style={{ cursor: 'pointer', display: 'flex', justifyContent: 'space-between', alignItems: 'center', userSelect: 'none' }}>
{correctionField}
{dropdownOpen && (
{modalOptions.map(opt => (
e.target.style.background = opt === correctionField ? 'var(--primary-color)' : 'rgba(128,128,128,0.1)'}
onMouseLeave={(e) => e.target.style.background = opt === correctionField ? 'var(--primary-color)' : 'transparent'}
style={{
padding: '0.8rem 1rem',
borderRadius: '8px',
cursor: 'pointer',
transition: 'background 0.2s',
backgroundColor: correctionField === opt ? 'var(--primary-color)' : 'transparent',
color: correctionField === opt ? '#000' : 'var(--text-primary)',
fontWeight: correctionField === opt ? '600' : 'normal',
marginBottom: '2px'
}}
onClick={() => { setCorrectionField(opt); setDropdownOpen(false); }}>
{opt}
))}
)}
Informe com atenção o dado correto:
setShowModal(false)}>Cancelar
{
const limitStr = encodeURIComponent(`Olá equipe iaagro!\n\nGostaria de abrir um ticket para correção manual de dados.\n\n*CNPJ:* ${formatCNPJ(user.cnpj)}\n*Campo Inconsistente:* ${correctionField}\n*Dado Atualizado / Informação Correta:* ${correctionData}\n\nAguardo a aprovação.`);
window.open(`https://wa.me/551530422070?text=${limitStr}`, '_blank');
setShowModal(false);
setCorrectionData('');
}}>Enviar via WhatsApp
)}
);
}
function AuthorizedNumbersView({ user, notify }) {
const { id: userId, plan_key: planKey } = user;
const [numbers, setNumbers] = useState([]);
const [loading, setLoading] = useState(true);
const [newName, setNewName] = useState('');
const [newPhone, setNewPhone] = useState('');
const limits = { produtor: 3, produtor_anual: 3, familia: 5, familia_anual: 5, cooperativa: 10, cooperativa_anual: 10, iniciante: 5 };
const maxNum = limits[planKey] || 0;
const fetchNumbers = async () => {
try {
const res = await fetch(`${API_URL}/authorized-numbers/${userId}`);
const data = await res.json();
if (res.ok && Array.isArray(data)) {
setNumbers(data);
} else {
setNumbers([]);
if (data.error) console.error(data.error);
}
} catch (err) { console.error(err); setNumbers([]); }
finally { setLoading(false); }
};
useEffect(() => { fetchNumbers(); }, []);
const handleAdd = async (e) => {
e.preventDefault();
if (!newName || !newPhone) return notify("Preencha nome e número.", "error");
try {
const res = await fetch(`${API_URL}/authorized-numbers/add`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, name: newName, phone: newPhone })
});
const data = await res.json();
if (res.ok && data.success) {
setNewName(''); setNewPhone('');
fetchNumbers();
notify("Número autorizado com sucesso!", "success");
} else {
notify(data.error || "Erro ao adicionar.", "error");
}
} catch (err) { console.error(err); notify("Erro no servidor.", "error"); }
};
const handleDelete = async (id) => {
if (!confirm("Deseja remover esta autorização?")) return;
try {
const res = await fetch(`${API_URL}/authorized-numbers/${id}`, { method: 'DELETE' });
if (res.ok) {
fetchNumbers();
notify("Autorização removida.", "info");
}
} catch (err) { console.error(err); }
};
return (
Números Autorizados
Gerencie quem pode emitir notas fiscais pelo WhatsApp em seu nome.
0 && numbers.length >= maxNum ? 'var(--danger)' : 'var(--primary-color)' }}>{numbers.length} / {maxNum || 0}
Números em uso
Adicionar Nova Autorização
Lista de Autorizados
{loading ?
Carregando...
: (
Nome
Número (WhatsApp)
Data Autorização
Ações
{numbers.map(n => (
{n.name}
{n.phone}
{new Date(n.created_at).toLocaleDateString()}
handleDelete(n.id)}>
))}
{numbers.length === 0 && (
Ninguém autorizado ainda.
)}
)}
);
}
function PlanoView({ user, notify }) {
const [loading, setLoading] = useState(false);
const [currentUser, setCurrentUser] = useState(user);
const [showPayForm, setShowPayForm] = useState(null); // 'iniciante', 'profissional', etc
const [cardData, setCardData] = useState({ holderName: '', number: '', expiry: '', cvv: '' });
const [billingCycle, setBillingCycle] = React.useState('annual');
useEffect(() => {
// Atualiza dados do usuário para pegar plano/vencimento novo
fetch(`${API_URL}/user/${user.id}`)
.then(res => res.json())
.then(data => { if (data.id) setCurrentUser(data); });
}, []);
const handleSubscribe = async (planKey) => {
setLoading(true);
try {
const response = await fetch(`${API_URL}/subscription/checkout`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: user.id, planKey })
});
const data = await response.json();
if (response.ok) {
if (data.invoiceUrl) {
const width = 600;
const height = 800;
const left = (window.screen.width / 2) - (width / 2);
const top = (window.screen.height / 2) - (height / 2);
const popup = window.open(data.invoiceUrl, 'AsaasCheckout', `width=${width},height=${height},top=${top},left=${left}`);
if (!popup || popup.closed || typeof popup.closed === 'undefined') {
notify("Checkout bloqueado pelo navegador. Por favor, libere popups e tente novamente.", "error");
// Adicional: Abrir fallback em aba normal
window.open(data.invoiceUrl, '_blank');
} else {
notify("Janela de pagamento aberta! Aguarde a conclusão.", "info");
}
// Polling para verificar quando o plano muda
const checkStatus = setInterval(async () => {
const res = await fetch(`${API_URL}/user/${user.id}`);
const updated = await res.json();
// Ele só deve dar confirmed se o pagamento aprovar e o plano mudar no banco
if (updated.subscription_status === 'ACTIVE' && updated.plan_key === planKey) {
setCurrentUser(updated);
notify("Pagamento confirmado! Plano ativado com sucesso.", "success");
clearInterval(checkStatus);
}
}, 5000);
// Limpar polling após 10 minutos
setTimeout(() => clearInterval(checkStatus), 600000);
} else {
notify("Requisição concluída.", "success");
}
} else {
notify(data.error || "Erro ao processar assinatura.", "error");
}
} catch (err) {
notify("Erro de conexão.", "error");
} finally {
setLoading(false);
}
};
const isCurrentPlan = (key) => currentUser.plan_key === key;
const isScheduled = (key) => currentUser.next_plan_key === key;
return (
Gestão de Assinatura
Status atual:
{currentUser.subscription_status === 'ACTIVE'
? `Pago - Válido até ${new Date(currentUser.due_date).toLocaleDateString()}`
: 'Aguardando Pagamento / Inativo'}
Cota de Emissão: {currentUser.notes_used || 0} / {currentUser.notes_limit > 1000 ? 'Ilimitado' : (currentUser.notes_limit || 0)} notas
{/* --- Toggle Mensal/Anual (Removido, plano fixo no anual) --- */}
{(isCurrentPlan('produtor') || isCurrentPlan('produtor_anual')) &&
Seu Plano
}
Produtor
R$ {billingCycle === 'monthly' ? '74,90' : '64,90'}/mês
{billingCycle === 'monthly' ? 'mensal' : 'mensal (faturado anualmente)'}
{billingCycle === 'annual' && 1 Certificado A1 Grátis }
30 Emissões mensais
1 CNPJ/CPF
3 Números Autorizados
{billingCycle === 'monthly' ? (
handleSubscribe('produtor')}
>
{isCurrentPlan('produtor') ? 'Plano Atual' : (isScheduled('produtor') ? 'Troca Agendada' : 'Assinar Mensal')}
) : (
handleSubscribe('produtor_anual')}
>
{isCurrentPlan('produtor_anual') ? 'Plano Atual' : (isScheduled('produtor_anual') ? 'Troca Agendada' : 'Assinar Anual')}
)}
{(isCurrentPlan('familia') || isCurrentPlan('familia_anual')) ?
Seu Plano
:
Recomendado
}
Família
R$ {billingCycle === 'monthly' ? '104,90' : '94,90'}/mês
{billingCycle === 'monthly' ? 'mensal' : 'mensal (faturado anualmente)'}
{billingCycle === 'annual' && 1 Certificado A1 Grátis }
100 Emissões mensais
3 CNPJ/CPF
5 Números Autorizados
{billingCycle === 'monthly' ? (
handleSubscribe('familia')}
>
{isCurrentPlan('familia') ? 'Plano Atual' : (isScheduled('familia') ? 'Troca Agendada' : 'Assinar Mensal')}
) : (
handleSubscribe('familia_anual')}
>
{isCurrentPlan('familia_anual') ? 'Plano Atual' : (isScheduled('familia_anual') ? 'Troca Agendada' : 'Assinar Anual')}
)}
{(isCurrentPlan('cooperativa') || isCurrentPlan('cooperativa_anual')) &&
Seu Plano
}
Cooperativa
R$ {billingCycle === 'monthly' ? '174,90' : '154,90'}/mês
{billingCycle === 'monthly' ? 'mensal' : 'mensal (faturado anualmente)'}
{billingCycle === 'annual' && 1 Certificado A1 Grátis }
500 Emissões mensais
CNPJ/CPF Ilimitado
10 Números Autorizados
{billingCycle === 'monthly' ? (
handleSubscribe('cooperativa')}
>
{isCurrentPlan('cooperativa') ? 'Plano Atual' : (isScheduled('cooperativa') ? 'Troca Agendada' : 'Assinar Mensal')}
) : (
handleSubscribe('cooperativa_anual')}
>
{isCurrentPlan('cooperativa_anual') ? 'Plano Atual' : (isScheduled('cooperativa_anual') ? 'Troca Agendada' : 'Assinar Anual')}
)}
);
}
// === COMPONENTE DE FUNDO INTERATIVO ===
function InteractiveBackground() {
const glowRef = React.useRef(null);
const gridRef = React.useRef(null);
useEffect(() => {
let mouseX = window.innerWidth / 2;
let mouseY = window.innerHeight / 2;
const handleMouseMove = (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
};
window.addEventListener('mousemove', handleMouseMove);
let animationFrameId;
let currentX = mouseX;
let currentY = mouseY;
const render = () => {
currentX += (mouseX - currentX) * 0.06;
currentY += (mouseY - currentY) * 0.06;
if (glowRef.current) {
glowRef.current.style.transform = `translate(calc(${currentX}px - 50%), calc(${currentY}px - 50%))`;
}
if (gridRef.current) {
// Mascara o GRID iluminando somente a área envolta do cursor dinamicamente
const maskStyle = `radial-gradient(circle 350px at ${currentX}px ${currentY}px, black 0%, transparent 100%)`;
gridRef.current.style.maskImage = maskStyle;
gridRef.current.style.WebkitMaskImage = maskStyle;
}
animationFrameId = requestAnimationFrame(render);
};
render();
return () => {
window.removeEventListener('mousemove', handleMouseMove);
cancelAnimationFrame(animationFrameId);
};
}, []);
return (
{/* O Grid Transparente que é revelado pelo mouse */}
{/* Orbe dinâmico de luz verde/neon */}
);
}
function CheckoutResult({ type, onNavigate, theme }) {
const logoSrc = theme === 'dark' ? 'assets/logo-dark.svg' : 'assets/logo-light.svg';
return (
{type === 'success' ? (
<>
Pagamento Concluído!
Parabéns! Sua assinatura foi ativada com sucesso. Você já pode emitir suas Notas Fiscais no WhatsApp com o poder da IA.
>
) : (
<>
Pagamento Cancelado
O processo de pagamento não foi concluído. Fique tranquilo, nenhuma cobrança foi realizada.
>
)}
onNavigate('portal')} style={{ width: '100%', padding: '1rem', fontSize: '1.1rem' }}>
Voltar para o Portal
);
}
// Render do React
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render( );