agendapro/admin/src/components/Plans.tsx

252 lines
11 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { Plus, X, Edit, Trash2, Check, Users, AlertTriangle } from 'lucide-react';
interface Plan {
id: string;
name: string;
slug: string;
priceMonthly: number;
priceYearly: number;
maxProfessionals: number;
maxServices: number;
features: string[];
active: boolean;
subscribers: number;
}
const emptyPlan: Partial<Plan> = {
name: '',
slug: '',
priceMonthly: 0,
priceYearly: 0,
maxProfessionals: 1,
maxServices: 10,
features: [],
active: true
};
export default function Plans() {
const [plans, setPlans] = useState<Plan[]>([]);
const [showModal, setShowModal] = useState(false);
const [editing, setEditing] = useState<Plan | null>(null);
const [form, setForm] = useState<Partial<Plan>>(emptyPlan);
const [featureInput, setFeatureInput] = useState('');
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const fetchPlans = async () => {
try {
const res = await fetch('/api/plans');
if (res.ok) {
const data = await res.json();
if (Array.isArray(data)) {
setPlans(data);
}
}
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchPlans();
}, []);
const openAdd = () => {
setEditing(null);
setForm({ ...emptyPlan, features: [] });
setShowModal(true);
};
const openEdit = (p: Plan) => {
setEditing(p);
setForm({ ...p, features: p.features || [] });
setShowModal(true);
};
const handleSave = async () => {
if (!form.name?.trim()) return;
const method = editing ? 'PUT' : 'POST';
try {
const res = await fetch('/api/plans', {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form)
});
if (res.ok) {
fetchPlans();
setShowModal(false);
}
} catch (e) {
console.error(e);
}
};
const handleDelete = async (id: string) => {
try {
await fetch(`/api/plans?id=${id}`, { method: 'DELETE' });
setPlans(prev => prev.filter(p => p.id !== id));
setDeleteConfirm(null);
} catch (e) {
console.error(e);
}
};
const addFeature = () => {
if (featureInput.trim()) {
setForm(p => ({ ...p, features: [...(p.features || []), featureInput.trim()] }));
setFeatureInput('');
}
};
const removeFeature = (idx: number) => {
setForm(p => ({ ...p, features: (p.features || []).filter((_, i) => i !== idx) }));
};
const colors = ['#3b82f6', '#6C63FF', '#a855f7', '#f59e0b'];
if (loading) return <div className="p-8 text-center" style={{color: 'var(--text-muted)'}}>Carregando planos...</div>;
return (
<div className="slide-up">
<div className="flex-between mb-6">
<p style={{ color: 'var(--text-secondary)', fontSize: '14px' }}>{plans.length} planos configurados</p>
<button className="btn btn-primary" onClick={openAdd}><Plus size={16} /> Novo Plano</button>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '20px' }}>
{plans.map((p, idx) => (
<div key={p.id} className="card" style={{ position: 'relative', borderTop: `3px solid ${colors[idx % colors.length]}` }}>
{deleteConfirm === p.id && (
<div style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.9)', borderRadius: 'var(--radius-lg)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 16, zIndex: 10 }}>
<AlertTriangle size={32} color="var(--warning)" />
<p style={{ fontWeight: 600 }}>Excluir plano {p.name}?</p>
<p style={{ fontSize: 12, color: 'var(--text-secondary)' }}>{p.subscribers} assinantes serão afetados</p>
<div style={{ display: 'flex', gap: 8 }}>
<button className="btn btn-sm btn-danger" onClick={() => handleDelete(p.id)}>Excluir</button>
<button className="btn btn-sm btn-secondary" onClick={() => setDeleteConfirm(null)}>Cancelar</button>
</div>
</div>
)}
<div className="flex-between mb-4">
<h3 style={{ fontSize: '20px', fontWeight: 800 }}>{p.name}</h3>
<div style={{ display: 'flex', gap: 4 }}>
<button className="btn-ghost btn-icon" onClick={() => openEdit(p)}><Edit size={14} /></button>
<button className="btn-ghost btn-icon" style={{ color: 'var(--danger)' }} onClick={() => setDeleteConfirm(p.id)}><Trash2 size={14} /></button>
</div>
</div>
<div style={{ marginBottom: '16px' }}>
<span style={{ fontSize: '32px', fontWeight: 900 }}>R$ {Number(p.priceMonthly || 0).toFixed(2).replace('.', ',')}</span>
<span style={{ fontSize: '14px', color: 'var(--text-muted)' }}>/mês</span>
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: 2 }}>ou R$ {Number(p.priceYearly || 0).toFixed(2).replace('.', ',')} /ano</div>
</div>
<div className="grid-2 mb-4">
<div style={{ padding: '10px', background: 'var(--bg-primary)', borderRadius: 'var(--radius-sm)', textAlign: 'center' }}>
<div style={{ fontSize: '16px', fontWeight: 800, color: colors[idx % colors.length] }}>{p.maxProfessionals >= 999 ? '∞' : p.maxProfessionals}</div>
<div style={{ fontSize: '10px', color: 'var(--text-muted)' }}>Profissionais</div>
</div>
<div style={{ padding: '10px', background: 'var(--bg-primary)', borderRadius: 'var(--radius-sm)', textAlign: 'center' }}>
<div style={{ fontSize: '16px', fontWeight: 800, color: colors[idx % colors.length] }}>{p.maxServices >= 999 ? '∞' : p.maxServices}</div>
<div style={{ fontSize: '10px', color: 'var(--text-muted)' }}>Serviços</div>
</div>
</div>
<ul style={{ listStyle: 'none', fontSize: '13px' }}>
{(p.features || []).map((f, i) => (
<li key={i} style={{ padding: '4px 0', color: 'var(--text-secondary)', display: 'flex', alignItems: 'center', gap: 6 }}>
<Check size={14} color="var(--success)" /> {f}
</li>
))}
</ul>
</div>
))}
</div>
{plans.length === 0 && (
<div className="p-8 text-center" style={{color: 'var(--text-muted)', border: '1px dashed var(--border-color)', borderRadius: 'var(--radius-lg)'}}>
Nenhum plano cadastrado.
</div>
)}
{showModal && (
<div className="modal-overlay" onClick={() => setShowModal(false)}>
<div className="modal" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h2 className="modal-title">{editing ? 'Editar Plano' : 'Novo Plano'}</h2>
<button className="btn-ghost btn-icon" onClick={() => setShowModal(false)}><X size={20} /></button>
</div>
<div className="modal-body">
<div className="grid-2">
<div className="form-group">
<label className="form-label">Nome *</label>
<input className="form-input" value={form.name || ''} onChange={e => {
const val = e.target.value;
setForm(p => ({
...p,
name: val,
slug: editing ? p.slug : val.toLowerCase().replace(/[^a-z0-9]/g, '-')
}));
}} />
</div>
<div className="form-group">
<label className="form-label">Slug</label>
<input className="form-input" value={form.slug || ''} onChange={e => setForm(p => ({ ...p, slug: e.target.value }))} disabled={!!editing} />
</div>
</div>
<div className="grid-2">
<div className="form-group">
<label className="form-label">Preço Mensal (R$)</label>
<input className="form-input" type="number" step="0.01" value={form.priceMonthly || 0} onChange={e => {
const val = Number(e.target.value);
setForm(p => ({ ...p, priceMonthly: val, priceYearly: p.priceYearly || val * 10 }));
}} />
</div>
<div className="form-group">
<label className="form-label">Preço Anual (R$)</label>
<input className="form-input" type="number" step="0.01" value={form.priceYearly || 0} onChange={e => setForm(p => ({ ...p, priceYearly: Number(e.target.value) }))} />
</div>
</div>
<div className="grid-2">
<div className="form-group">
<label className="form-label">Max Profissionais</label>
<input className="form-input" type="number" value={form.maxProfessionals || 1} onChange={e => setForm(p => ({ ...p, maxProfessionals: Number(e.target.value) }))} />
</div>
<div className="form-group">
<label className="form-label">Max Serviços</label>
<input className="form-input" type="number" value={form.maxServices || 10} onChange={e => setForm(p => ({ ...p, maxServices: Number(e.target.value) }))} />
</div>
</div>
<div className="form-group">
<label className="form-label">Recursos</label>
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
<input className="form-input" placeholder="Adicionar recurso..." value={featureInput} onChange={e => setFeatureInput(e.target.value)} onKeyDown={e => {
if (e.key === 'Enter') {
e.preventDefault();
addFeature();
}
}} />
<button type="button" className="btn btn-sm btn-secondary" onClick={addFeature}>+</button>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{(form.features || []).map((f, i) => (
<div key={i} className="flex-between" style={{ padding: '6px 10px', background: 'var(--bg-primary)', borderRadius: 'var(--radius-sm)', fontSize: 13 }}>
<span>{f}</span>
<button type="button" className="btn-ghost btn-icon" onClick={() => removeFeature(i)} style={{ color: 'var(--danger)' }}><X size={12} /></button>
</div>
))}
</div>
</div>
</div>
<div className="modal-footer">
<button className="btn btn-secondary" onClick={() => setShowModal(false)}>Cancelar</button>
<button className="btn btn-primary" onClick={handleSave}>{editing ? 'Salvar' : 'Criar Plano'}</button>
</div>
</div>
</div>
)}
</div>
);
}