agendapro/src/components/Subscription.tsx

510 lines
25 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState, useEffect } from 'react';
import { Check, Crown, Zap, Building2, Rocket, AlertTriangle, ShieldAlert, CreditCard, QrCode, FileText, CheckCircle2, ShieldCheck } from 'lucide-react';
const staticPlans = [
{
name: 'Starter', price: 49.90, icon: Zap, color: '#3b82f6',
features: ['1 profissional', '10 serviços', '200 agendamentos/mês', 'Agendamento online', 'Página de reservas', 'Lembretes por e-mail', 'Relatórios básicos']
},
{
name: 'Professional', price: 99.90, icon: Crown, color: '#6C63FF', featured: true,
features: ['5 profissionais', '30 serviços', 'Agendamentos ilimitados', 'Tudo do Starter', 'Múltiplos profissionais', 'Comissionamento', 'Relatórios avançados', 'Personalização da marca']
},
{
name: 'Business', price: 199.90, icon: Building2, color: '#a855f7',
features: ['15 profissionais', '100 serviços', 'Agendamentos ilimitados', 'Tudo do Professional', 'API de integração', 'Suporte prioritário', 'Multi-unidades', 'Automações avançadas']
},
{
name: 'Enterprise', price: 399.90, icon: Rocket, color: '#f59e0b',
features: ['Profissionais ilimitados', 'Serviços ilimitados', 'Agendamentos ilimitados', 'Tudo do Business', 'Gerente dedicado', 'SLA garantido', 'Integrações customizadas', 'White-label']
},
];
// Máscaras de entrada
const formatCpfCnpj = (value: string) => {
const v = value.replace(/\D/g, '');
if (v.length <= 11) {
return v.replace(/(\d{3})(\d)/, '$1.$2')
.replace(/(\d{3})(\d)/, '$1.$2')
.replace(/(\d{3})(\d{1,2})$/, '$1-$2');
}
return v.replace(/^(\d{2})(\d)/, '$1.$2')
.replace(/^(\d{2})\.(\d{3})(\d)/, '$1.$2.$3')
.replace(/\.(\d{3})(\d)/, '.$1/$2')
.replace(/(\d{4})(\d)/, '$1-$2')
.slice(0, 18);
};
const formatPhone = (value: string) => {
const v = value.replace(/\D/g, '');
return v.replace(/(\d{2})(\d)/, '($1) $2')
.replace(/(\d{5})(\d)/, '$1-$2')
.slice(0, 15);
};
const formatCep = (value: string) => {
const v = value.replace(/\D/g, '');
return v.replace(/(\d{5})(\d)/, '$1-$2')
.slice(0, 9);
};
const formatCardNumber = (value: string) => {
const v = value.replace(/\D/g, '');
return v.replace(/(\d{4})(?=\d)/g, '$1 ').slice(0, 19);
};
const formatExpiry = (value: string) => {
const v = value.replace(/\D/g, '');
return v.replace(/(\d{2})(\d)/, '$1/$2').slice(0, 5);
};
const formatCvv = (value: string) => {
return value.replace(/\D/g, '').slice(0, 4);
};
interface SubscriptionProps {
currentPlan: string;
onPlanChange: (newPlan: string) => void;
isLocked?: boolean;
onPaymentSuccess?: () => void;
}
export default function Subscription({ currentPlan, onPlanChange, isLocked = false, onPaymentSuccess }: SubscriptionProps) {
const [selectedPlan, setSelectedPlan] = useState<any>(null);
const [paymentMethod, setPaymentMethod] = useState<'PIX' | 'BOLETO' | 'CREDIT_CARD'>('PIX');
const [loading, setLoading] = useState(false);
const [successData, setSuccessData] = useState<any>(null);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const [dbPlans, setDbPlans] = useState<any[]>([]);
useEffect(() => {
const fetchSubscriptionData = async () => {
const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
try {
const res = await fetch(`/api/tenant/subscription?slug=${slug}`);
if (res.ok) {
const data = await res.json();
if (data.plans && data.plans.length > 0) {
setDbPlans(data.plans);
}
}
} catch (e) {
console.error('Erro ao buscar planos da API:', e);
}
};
fetchSubscriptionData();
}, []);
const iconMap: Record<string, any> = { Starter: Zap, Professional: Crown, Business: Building2, Enterprise: Rocket };
const colorMap: Record<string, string> = { Starter: '#3b82f6', Professional: '#6C63FF', Business: '#a855f7', Enterprise: '#f59e0b' };
const plans = dbPlans.length > 0 ? dbPlans.map(p => {
let featuresList: string[] = [];
try {
if (typeof p.features === 'string') featuresList = JSON.parse(p.features);
else if (Array.isArray(p.features)) featuresList = p.features;
} catch (e) {}
return {
id: p.id,
name: p.name,
price: Number(p.price || p.price_monthly || 0),
icon: iconMap[p.name] || Crown,
color: colorMap[p.name] || '#6C63FF',
featured: p.name === 'Professional',
features: featuresList.length > 0 ? featuresList : ['Agendamento online']
};
}) : staticPlans;
// Form de cartão
const [cardHolder, setCardHolder] = useState('');
const [cardNumber, setCardNumber] = useState('');
const [cardExpiry, setCardExpiry] = useState('');
const [cardCvv, setCardCvv] = useState('');
// Dados de Faturamento
const [billingInfo, setBillingInfo] = useState({
name: '',
cpfCnpj: '',
phone: '',
email: '',
postalCode: '',
addressNumber: '',
});
const activePlanData = plans.find(p => p.name === currentPlan) || plans[1];
const handleOpenPayment = (plan: any) => {
setSelectedPlan(plan);
setSuccessData(null);
setErrorMsg(null);
};
const handlePay = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setErrorMsg(null);
const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
let creditCard = undefined;
let creditCardHolderInfo = undefined;
if (paymentMethod === 'CREDIT_CARD') {
const [month, year] = cardExpiry.split('/');
creditCard = {
holderName: cardHolder,
number: cardNumber.replace(/\s/g, ''),
expiryMonth: month || '12',
expiryYear: year ? `20${year}` : '2030',
ccv: cardCvv,
};
creditCardHolderInfo = {
name: cardHolder,
email: `${slug}@agendapro.com`,
cpfCnpj: '00000000000',
postalCode: '01310000',
addressNumber: '123',
phone: '11999991234',
};
}
try {
const res = await fetch('/api/tenant/subscription', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
slug,
planName: selectedPlan.name,
billingType: paymentMethod,
billingInfo,
creditCard,
creditCardHolderInfo,
}),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || 'Falha ao processar pagamento');
}
setSuccessData(data);
onPlanChange(selectedPlan.name);
} catch (err: any) {
console.error(err);
setErrorMsg(err.message || 'Erro inesperado no servidor');
} finally {
setLoading(false);
}
};
// Simulação de pagamento no Sandbox / Dev
const handleSimulatePayment = async () => {
setLoading(true);
const slug = localStorage.getItem('agendapro_tenant_slug') || 'barbearia-premium';
// Configura o cookie e localStorage de homologação local
localStorage.setItem('agendapro_subscription_status', 'active');
document.cookie = "agendapro_subscription_status=active; path=/; max-age=2592000";
try {
const res = await fetch('/api/tenant/subscription/simulate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slug, status: 'active' }),
});
alert('Pagamento simulado com sucesso! O sistema foi desbloqueado.');
setSelectedPlan(null);
if (onPaymentSuccess) {
onPaymentSuccess();
}
} catch (err) {
console.warn('Erro ao conectar com API de simulação, utilizando fallback local de homologação.', err);
alert('Pagamento simulado localmente! O sistema foi desbloqueado (modo offline).');
setSelectedPlan(null);
if (onPaymentSuccess) {
onPaymentSuccess();
}
} finally {
setLoading(false);
}
};
return (
<div className="slide-up">
{/* BANNER DE BLOQUEIO */}
{isLocked && (
<div style={{
background: 'linear-gradient(135deg, rgba(239,68,68,0.15), rgba(245,158,11,0.1))',
border: '2px solid rgba(239,68,68,0.4)', borderRadius: 'var(--radius-lg)',
padding: '20px', marginBottom: '28px', display: 'flex', gap: '16px', alignItems: 'center'
}}>
<div style={{ background: 'var(--danger)', borderRadius: '50%', width: 44, height: 44, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<ShieldAlert size={24} color="white" />
</div>
<div>
<h4 style={{ fontWeight: 800, color: 'var(--danger)', fontSize: '16px', marginBottom: '4px' }}>Acesso Pendente / Bloqueado 🔒</h4>
<p style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>
Sua assinatura do AgendaPRO não está ativa. Para reativar seu acesso completo e continuar gerenciando seus agendamentos, clientes e equipe, escolha um plano abaixo e efetue o pagamento.
</p>
</div>
</div>
)}
{/* PLANO ATUAL INFO */}
{!isLocked && (
<div className="card mb-6" style={{ background: 'var(--gradient-primary)', border: 'none' }}>
<div className="flex-between">
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '8px' }}>
<Crown size={24} color="white" />
<h2 style={{ fontSize: '22px', fontWeight: 800, color: 'white' }}>Plano {activePlanData.name}</h2>
</div>
<p style={{ color: 'rgba(255,255,255,0.9)' }}>Sua assinatura está ativa e regularizada.</p>
</div>
<div style={{ textAlign: 'right', color: 'white' }}>
<div style={{ fontSize: '32px', fontWeight: 900 }}>
R$ {activePlanData.price.toFixed(2).replace('.', ',')}
<span style={{ fontSize: '14px', fontWeight: 400 }}>/mês</span>
</div>
<span className="badge" style={{ background: 'rgba(255,255,255,0.2)', color: 'white' }}>Ativo</span>
</div>
</div>
</div>
)}
<h3 style={{ fontSize: '18px', fontWeight: 700, marginBottom: '20px' }}>Escolha o plano ideal</h3>
<div className="pricing-grid">
{plans.map(plan => {
const Icon = plan.icon;
const isActive = currentPlan === plan.name && !isLocked;
return (
<div key={plan.name} className={`pricing-card ${isActive ? 'featured' : ''}`} style={isLocked ? { border: '1px solid var(--border-color)' } : {}}>
<div style={{ width: 56, height: 56, borderRadius: 'var(--radius-md)', background: `${plan.color}20`, display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 16px' }}>
<Icon size={28} color={plan.color} />
</div>
<div className="pricing-name">{plan.name}</div>
<div className="pricing-price">
R$ {plan.price.toFixed(2).replace('.', ',')}
<span>/mês</span>
</div>
<ul className="pricing-features">
{plan.features.map(f => (
<li key={f}><Check size={16} /> {f}</li>
))}
</ul>
<button
className={`btn ${isActive ? 'btn-primary' : 'btn-secondary'}`}
style={{ width: '100%', justifyContent: 'center' }}
onClick={() => handleOpenPayment(plan)}
>
{isActive ? 'Plano Atual (Pagar de Novo)' : 'Assinar Plano'}
</button>
</div>
);
})}
</div>
{/* MODAL DE PAGAMENTO */}
{selectedPlan && (
<div className="modal-overlay" onClick={() => setSelectedPlan(null)}>
<div className="modal" onClick={e => e.stopPropagation()} style={{ maxWidth: '500px' }}>
<div className="modal-header">
<h2 className="modal-title">Assinar Plano {selectedPlan.name}</h2>
<button className="btn-ghost btn-icon" onClick={() => setSelectedPlan(null)}></button>
</div>
<div className="modal-body">
{!successData ? (
<form onSubmit={handlePay} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
{/* Seleção de meio de pagamento */}
<div>
<label className="form-label">Meio de Pagamento</label>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '8px' }}>
<button type="button" className={`btn ${paymentMethod === 'PIX' ? 'btn-primary' : 'btn-secondary'}`}
style={{ flexDirection: 'column', padding: '12px 6px', gap: '4px', fontSize: '13px' }}
onClick={() => setPaymentMethod('PIX')}>
<QrCode size={18} /> Pix
</button>
<button type="button" className={`btn ${paymentMethod === 'BOLETO' ? 'btn-primary' : 'btn-secondary'}`}
style={{ flexDirection: 'column', padding: '12px 6px', gap: '4px', fontSize: '13px' }}
onClick={() => setPaymentMethod('BOLETO')}>
<FileText size={18} /> Boleto
</button>
<button type="button" className={`btn ${paymentMethod === 'CREDIT_CARD' ? 'btn-primary' : 'btn-secondary'}`}
style={{ flexDirection: 'column', padding: '12px 6px', gap: '4px', fontSize: '13px' }}
onClick={() => setPaymentMethod('CREDIT_CARD')}>
<CreditCard size={18} /> Cartão
</button>
</div>
</div>
<div style={{ borderTop: '1px solid var(--border-color)', paddingTop: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ color: 'var(--text-secondary)' }}>Plano:</span>
<strong style={{ color: 'var(--text-primary)' }}>{selectedPlan.name} (Mensal)</strong>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-secondary)' }}>Valor total:</span>
<strong style={{ color: 'var(--accent)', fontSize: '18px' }}>R$ {selectedPlan.price.toFixed(2).replace('.', ',')}</strong>
</div>
</div>
{/* Dados de Faturamento */}
<div style={{ borderTop: '1px solid var(--border-color)', paddingTop: '16px', display: 'flex', flexDirection: 'column', gap: '12px' }}>
<h4 style={{ fontSize: '14px', fontWeight: 600, color: 'var(--text-primary)', marginBottom: '4px' }}>Dados do Pagador</h4>
<div className="form-group" style={{ margin: 0 }}>
<label className="form-label">Nome Completo / Razão Social</label>
<input required className="form-input" placeholder="João da Silva" value={billingInfo.name} onChange={e => setBillingInfo({...billingInfo, name: e.target.value})} />
</div>
<div className="grid-2">
<div className="form-group" style={{ margin: 0 }}>
<label className="form-label">CPF ou CNPJ</label>
<input required className="form-input" placeholder="000.000.000-00" value={billingInfo.cpfCnpj} onChange={e => setBillingInfo({...billingInfo, cpfCnpj: formatCpfCnpj(e.target.value)})} />
</div>
<div className="form-group" style={{ margin: 0 }}>
<label className="form-label">Celular</label>
<input required className="form-input" placeholder="(00) 90000-0000" value={billingInfo.phone} onChange={e => setBillingInfo({...billingInfo, phone: formatPhone(e.target.value)})} />
</div>
</div>
<div className="form-group" style={{ margin: 0 }}>
<label className="form-label">E-mail</label>
<input type="email" required className="form-input" placeholder="seu@email.com" value={billingInfo.email} onChange={e => setBillingInfo({...billingInfo, email: e.target.value})} />
</div>
{paymentMethod === 'CREDIT_CARD' && (
<div className="grid-2">
<div className="form-group" style={{ margin: 0 }}>
<label className="form-label">CEP</label>
<input required className="form-input" placeholder="00000-000" value={billingInfo.postalCode} onChange={e => setBillingInfo({...billingInfo, postalCode: formatCep(e.target.value)})} />
</div>
<div className="form-group" style={{ margin: 0 }}>
<label className="form-label">Número</label>
<input required className="form-input" placeholder="123" value={billingInfo.addressNumber} onChange={e => setBillingInfo({...billingInfo, addressNumber: e.target.value})} />
</div>
</div>
)}
</div>
{/* Inputs específicos do Cartão */}
{paymentMethod === 'CREDIT_CARD' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', animation: 'fadeIn 0.2s' }}>
<div className="form-group" style={{ margin: 0 }}>
<label className="form-label">Nome do Titular (como no cartão)</label>
<input required className="form-input" placeholder="Nome Completo" value={cardHolder} onChange={e => setCardHolder(e.target.value)} />
</div>
<div className="form-group" style={{ margin: 0 }}>
<label className="form-label">Número do Cartão</label>
<input required className="form-input" placeholder="0000 0000 0000 0000" value={cardNumber} onChange={e => setCardNumber(formatCardNumber(e.target.value))} />
</div>
<div className="grid-2">
<div className="form-group" style={{ margin: 0 }}>
<label className="form-label">Validade (MM/AA)</label>
<input required className="form-input" placeholder="MM/AA" value={cardExpiry} onChange={e => setCardExpiry(formatExpiry(e.target.value))} />
</div>
<div className="form-group" style={{ margin: 0 }}>
<label className="form-label">CVV</label>
<input required className="form-input" placeholder="000" value={cardCvv} onChange={e => setCardCvv(formatCvv(e.target.value))} />
</div>
</div>
</div>
)}
{errorMsg && (
<div style={{ color: 'var(--danger)', fontSize: '13px', background: 'rgba(239,68,68,0.1)', padding: '10px', borderRadius: 'var(--radius-sm)', border: '1px solid var(--danger)' }}>
{errorMsg}
</div>
)}
<button type="submit" className="btn btn-primary" style={{ width: '100%', justifyContent: 'center', padding: '12px' }} disabled={loading}>
{loading ? 'Processando transação...' : `Confirmar e Pagar R$ ${selectedPlan.price.toFixed(2).replace('.', ',')}`}
</button>
</form>
) : (
/* RESPOSTA DE SUCESSO DE PAGAMENTO */
<div style={{ textAlign: 'center', padding: '12px 0', animation: 'fadeIn 0.3s' }}>
{paymentMethod === 'PIX' && successData.pixCode && (
<div>
<div style={{ width: 44, height: 44, background: 'var(--success-bg)', color: 'var(--success)', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 16px' }}>
<CheckCircle2 size={24} />
</div>
<h3 style={{ fontSize: '18px', fontWeight: 800, marginBottom: '8px' }}>Cobrança Pix Gerada!</h3>
<p style={{ fontSize: '13px', color: 'var(--text-secondary)', marginBottom: '20px' }}>Escaneie o QR Code abaixo com seu banco para efetuar o pagamento da assinatura.</p>
{successData.pixQrCodeImage && (
<div style={{ background: 'white', padding: '16px', width: '200px', height: '200px', margin: '0 auto 20px', borderRadius: 'var(--radius-md)', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid var(--border-color)' }}>
<img src={`data:image/png;base64,${successData.pixQrCodeImage}`} alt="QR Code Pix" style={{ width: '100%', height: '100%' }} />
</div>
)}
<div className="form-group">
<label className="form-label" style={{ fontSize: '11px', textTransform: 'uppercase' }}>Código Copia e Cola</label>
<div style={{ display: 'flex', gap: '8px' }}>
<input readOnly className="form-input" style={{ fontSize: '12px' }} value={successData.pixCode} />
<button className="btn btn-secondary" onClick={() => { navigator.clipboard.writeText(successData.pixCode); alert('Código copiado!'); }}>Copiar</button>
</div>
</div>
</div>
)}
{paymentMethod === 'BOLETO' && (
<div>
<div style={{ width: 44, height: 44, background: 'var(--success-bg)', color: 'var(--success)', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 16px' }}>
<CheckCircle2 size={24} />
</div>
<h3 style={{ fontSize: '18px', fontWeight: 800, marginBottom: '8px' }}>Boleto Emitido com Sucesso!</h3>
<p style={{ fontSize: '13px', color: 'var(--text-secondary)', marginBottom: '24px' }}>
O boleto de cobrança foi emitido. Você receberá um e-mail com as instruções, ou poderá realizar o pagamento usando o link do painel de controle.
</p>
<button className="btn btn-primary" style={{ margin: '0 auto' }} onClick={() => window.open('https://sandbox.asaas.com', '_blank')}>
Visualizar Boleto PDF
</button>
</div>
)}
{paymentMethod === 'CREDIT_CARD' && (
<div>
<div style={{ width: 44, height: 44, background: 'var(--success-bg)', color: 'var(--success)', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 16px' }}>
<ShieldCheck size={24} />
</div>
<h3 style={{ fontSize: '18px', fontWeight: 800, marginBottom: '8px' }}>Transação Aprovada!</h3>
<p style={{ fontSize: '13px', color: 'var(--text-secondary)', marginBottom: '24px' }}>
Sua assinatura do plano <strong>{selectedPlan.name}</strong> foi confirmada e está ativa. Obrigado!
</p>
<button className="btn btn-primary" style={{ margin: '0 auto' }} onClick={() => { setSelectedPlan(null); if (onPaymentSuccess) onPaymentSuccess(); }}>
Entrar no Sistema
</button>
</div>
)}
</div>
)}
</div>
{/* BARRA DE FERRAMENTAS DE SIMULAÇÃO (SANDBOX DEVELOPER MODE) */}
<div className="modal-footer" style={{ background: 'var(--bg-elevated)', flexDirection: 'column', gap: '12px', alignItems: 'stretch' }}>
<div style={{ fontSize: '12px', color: 'var(--text-muted)', textAlign: 'center', display: 'flex', alignItems: 'center', gap: '6px', justifyContent: 'center' }}>
<ShieldCheck size={14} color="var(--warning)" /> Modo Homologação Sandbox Ativo
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button className="btn btn-secondary" style={{ flex: 1 }} onClick={() => setSelectedPlan(null)}>
Fechar Janela
</button>
<button className="btn btn-primary" style={{ flex: 1.5, background: 'var(--warning)', border: 'none', color: 'black', fontWeight: 700 }}
onClick={handleSimulatePayment} disabled={loading}>
Simular Pagamento Webhook
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
}