foodsnap/src/pages/AdminPanel.tsx

798 lines
45 KiB
TypeScript
Raw Normal View History

import React, { useEffect, useState } from 'react';
import {
LayoutDashboard,
Users,
CreditCard,
LogOut,
ArrowLeft,
TrendingUp,
Ticket,
Search,
ShieldAlert,
Download,
Plus,
DollarSign,
Calendar,
CheckCircle2,
XCircle,
MoreHorizontal,
UserPlus,
Activity,
AlertTriangle,
User,
Clock,
Info,
Settings,
Save,
Smartphone
} from 'lucide-react';
import { supabase } from '@/lib/supabase';
import { User as AppUser } from '@/types';
interface AdminPanelProps {
user: AppUser;
onExitAdmin: () => void;
onLogout: () => void;
}
// Tipos baseados nas novas tabelas SQL
type TabType = 'overview' | 'users' | 'financial' | 'coupons' | 'settings';
const AdminPanel: React.FC<AdminPanelProps> = ({ user, onExitAdmin, onLogout }) => {
const [activeTab, setActiveTab] = useState<TabType>('overview');
const [loading, setLoading] = useState(true);
// Data States
const [stats, setStats] = useState<any>(null);
const [usersList, setUsersList] = useState<any[]>([]);
const [coupons, setCoupons] = useState<any[]>([]);
// Settings State
const [config, setConfig] = useState({
whatsapp_number: '' // Inicializa vazio para não confundir
});
const [savingConfig, setSavingConfig] = useState(false);
// UI States
const [searchTerm, setSearchTerm] = useState('');
const [showCouponModal, setShowCouponModal] = useState(false);
const [newCoupon, setNewCoupon] = useState({ code: '', percent: 10, uses: 100 });
useEffect(() => {
fetchDashboardData();
}, []);
const fetchDashboardData = async () => {
setLoading(true);
try {
// 1. Stats
const { data: sData } = await supabase.rpc('get_admin_dashboard_stats');
if (sData) setStats(sData);
// 2. Users (Robust Fetch)
await fetchUsersSafe();
// 3. Coupons
const { data: cData } = await supabase.from('coupons').select('*').order('created_at', { ascending: false });
if (cData) setCoupons(cData);
// 4. Settings
const { data: configData } = await supabase
.from('app_settings')
.select('value')
.eq('key', 'whatsapp_number')
.maybeSingle();
if (configData) {
setConfig({ whatsapp_number: configData.value });
} else {
// Fallback visual apenas se não tiver nada no banco
setConfig({ whatsapp_number: '5541999999999' });
}
} catch (error) {
console.error("Admin fetch error", error);
} finally {
setLoading(false);
}
};
const fetchUsersSafe = async () => {
// Tenta usar a função avançada (RPC) que tem dados do plano
const { data: rpcData, error: rpcError } = await supabase.rpc('get_admin_users_list', { limit_count: 50 });
if (!rpcError && rpcData) {
setUsersList(rpcData);
return;
}
// Se falhar (ex: SQL não atualizado), busca o básico da tabela profiles para não deixar a tela vazia
console.warn("RPC falhou, usando fallback de perfis:", rpcError);
const { data: basicData } = await supabase
.from('profiles')
.select('*')
.order('created_at', { ascending: false })
.limit(50);
if (basicData) {
const mapped = basicData.map(p => ({
id: p.id,
full_name: p.full_name,
email: p.email,
phone: p.phone_e164,
created_at: p.created_at,
plan_status: 'free',
plan_interval: 'free',
lifetime_value: 0,
plan_start_date: null,
plan_end_date: null
}));
setUsersList(mapped);
}
};
const handleCreateCoupon = async (e: React.FormEvent) => {
e.preventDefault();
try {
const { error } = await supabase.rpc('admin_create_coupon', {
p_code: newCoupon.code,
p_percent: newCoupon.percent,
p_uses: newCoupon.uses
});
if (error) throw error;
const { data: cData } = await supabase.from('coupons').select('*').order('created_at', { ascending: false });
if (cData) setCoupons(cData);
setShowCouponModal(false);
setNewCoupon({ code: '', percent: 10, uses: 100 });
alert("Cupom criado com sucesso!");
} catch (err: any) {
alert("Erro ao criar cupom: " + err.message);
}
};
const handleToggleProfessional = async (userId: string, newValue: boolean) => {
try {
const { error } = await supabase
.from('profiles')
.update({ is_professional: newValue })
.eq('id', userId);
if (error) throw error;
// Optimistic update
setUsersList(prev => prev.map(u =>
u.id === userId ? { ...u, is_professional: newValue } : u
));
// If enhancing to Professional, check/create the professionals record
if (newValue) {
const { data: existing } = await supabase.from('professionals').select('id').eq('id', userId).maybeSingle();
if (!existing) {
// Auto-init profile
// We don't have the user name here easily unless we look it up,
// but we can trust the 'professionals' RLS or just let them create it on first login.
// Ideally, we create a stub here.
const user = usersList.find(u => u.id === userId);
if (user) {
await supabase.from('professionals').insert({
id: userId,
business_name: user.full_name || 'Novo Profissional',
primary_color: '#059669' // Default Green
});
}
}
}
// toast.success(`Status alterado para ${newValue ? 'Profissional' : 'Aluno'}`);
} catch (error) {
console.error("Error toggling pro status:", error);
alert("Erro ao alterar status!");
}
};
const handleSaveSettings = async (e: React.FormEvent) => {
e.preventDefault();
setSavingConfig(true);
try {
const { error } = await supabase
.from('app_settings')
.upsert({ key: 'whatsapp_number', value: config.whatsapp_number }, { onConflict: 'key' });
if (error) throw error;
alert("Configurações salvas com sucesso!");
} catch (err: any) {
console.error(err);
alert("Erro ao salvar: " + err.message);
} finally {
setSavingConfig(false);
}
};
// Safe filter logic (handles null names)
const filteredUsers = usersList.filter(u =>
(u.full_name || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
(u.email || '').toLowerCase().includes(searchTerm.toLowerCase())
);
const formatCurrency = (cents: number) => {
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(cents / 100);
};
const formatDate = (dateStr: string | null) => {
if (!dateStr) return '-';
return new Date(dateStr).toLocaleDateString('pt-BR');
};
return (
<div className="min-h-screen bg-[#F3F4F6] font-sans text-gray-900 flex">
{/* Sidebar Premium */}
<aside className="w-72 bg-gray-900 text-white fixed h-full z-30 hidden lg:flex flex-col shadow-2xl">
<div className="p-8 border-b border-gray-800">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-brand-500 to-brand-700 rounded-xl flex items-center justify-center text-white shadow-lg shadow-brand-500/20">
<ShieldAlert size={20} />
</div>
<div>
<span className="font-bold text-xl tracking-tight block">FoodSnap</span>
<span className="text-[10px] text-gray-400 font-mono uppercase tracking-widest bg-gray-800 px-2 py-0.5 rounded-full">Master Admin</span>
</div>
</div>
</div>
<nav className="flex-1 p-6 space-y-2">
<NavButton
active={activeTab === 'overview'}
onClick={() => setActiveTab('overview')}
icon={<LayoutDashboard size={20} />}
label="Dashboard"
/>
<NavButton
active={activeTab === 'users'}
onClick={() => setActiveTab('users')}
icon={<Users size={20} />}
label="Usuários & Planos"
/>
<NavButton
active={activeTab === 'financial'}
onClick={() => setActiveTab('financial')}
icon={<DollarSign size={20} />}
label="Financeiro"
/>
<NavButton
active={activeTab === 'coupons'}
onClick={() => setActiveTab('coupons')}
icon={<Ticket size={20} />}
label="Cupons & Ofertas"
/>
<NavButton
active={activeTab === 'settings'}
onClick={() => setActiveTab('settings')}
icon={<Settings size={20} />}
label="Configurações"
/>
</nav>
<div className="p-6 border-t border-gray-800 space-y-2 bg-gray-950/30">
<button
onClick={onExitAdmin}
className="w-full flex items-center gap-3 px-4 py-3 rounded-lg text-gray-400 hover:text-white hover:bg-gray-800 transition-colors text-sm font-medium"
>
<ArrowLeft size={18} />
Voltar ao App
</button>
<button
onClick={onLogout}
className="w-full flex items-center gap-3 px-4 py-3 rounded-lg text-red-400 hover:text-red-300 hover:bg-red-900/20 transition-colors text-sm font-medium"
>
<LogOut size={18} />
Sair
</button>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 lg:ml-72 p-6 md:p-10 overflow-y-auto">
{loading ? (
<div className="flex items-center justify-center h-full">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-brand-600"></div>
</div>
) : (
<div className="max-w-7xl mx-auto space-y-8">
{/* Top Bar */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-gray-900 tracking-tight">
{activeTab === 'overview' && 'Visão Geral'}
{activeTab === 'users' && 'Gestão de Usuários'}
{activeTab === 'financial' && 'Controle Financeiro'}
{activeTab === 'coupons' && 'Cupons de Desconto'}
{activeTab === 'settings' && 'Configurações do Sistema'}
</h1>
<p className="text-gray-500 mt-1 flex items-center gap-2 text-sm">
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse"></span>
Sistema Operacional {new Date().toLocaleDateString()}
</p>
</div>
<div className="flex gap-3">
{activeTab !== 'settings' && (
<button className="bg-white border border-gray-200 text-gray-700 px-4 py-2 rounded-lg text-sm font-medium shadow-sm hover:bg-gray-50 flex items-center gap-2">
<Download size={16} /> Exportar Relatório
</button>
)}
{activeTab === 'coupons' && (
<button
onClick={() => setShowCouponModal(true)}
className="bg-brand-600 text-white px-4 py-2 rounded-lg text-sm font-medium shadow-md shadow-brand-500/20 hover:bg-brand-700 flex items-center gap-2"
>
<Plus size={16} /> Criar Cupom
</button>
)}
</div>
</div>
{/* --- OVERVIEW TAB --- */}
{activeTab === 'overview' && stats && (
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<KpiCard
title="Receita Total"
value={formatCurrency(stats.total_revenue || 0)}
icon={<DollarSign className="text-white" size={24} />}
color="bg-emerald-500"
trend="+8.2%"
/>
<KpiCard
title="Assinantes Ativos"
value={stats.active_subs}
icon={<CreditCard className="text-white" size={24} />}
color="bg-blue-500"
trend="+12"
/>
<KpiCard
title="Total Usuários"
value={stats.total_users}
icon={<Users className="text-white" size={24} />}
color="bg-indigo-500"
trend="+24"
/>
<KpiCard
title="Novos (24h)"
value={stats.new_users_24h}
icon={<UserPlus className="text-white" size={24} />}
color="bg-purple-500"
trend="Hoje"
/>
</div>
{/* Recent Activity Section */}
<div className="grid lg:grid-cols-3 gap-8">
{/* Chart placeholder area */}
<div className="lg:col-span-2 bg-white p-6 rounded-2xl shadow-sm border border-gray-200">
<h3 className="font-bold text-gray-900 mb-6">Crescimento de Receita (Simulado)</h3>
<div className="h-64 flex items-end gap-4">
{[40, 65, 50, 80, 75, 90, 85, 100].map((h, i) => (
<div key={i} className="flex-1 bg-brand-50 rounded-t-lg relative group">
<div
className="absolute bottom-0 left-0 right-0 bg-brand-500 rounded-t-lg transition-all duration-1000 group-hover:bg-brand-600"
style={{ height: `${h}%` }}
></div>
</div>
))}
</div>
<div className="flex justify-between mt-4 text-xs text-gray-400 font-medium uppercase">
<span>Jan</span><span>Fev</span><span>Mar</span><span>Abr</span><span>Mai</span><span>Jun</span><span>Jul</span><span>Ago</span>
</div>
</div>
{/* Quick Actions / Recent */}
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200">
<h3 className="font-bold text-gray-900 mb-4">Ações Rápidas</h3>
<div className="space-y-3">
<button onClick={() => setShowCouponModal(true)} className="w-full text-left p-3 rounded-xl bg-gray-50 hover:bg-gray-100 transition-colors flex items-center gap-3">
<div className="p-2 bg-purple-100 text-purple-600 rounded-lg"><Ticket size={18} /></div>
<div>
<p className="font-bold text-sm text-gray-800">Criar Novo Cupom</p>
<p className="text-xs text-gray-500">Impulsione vendas hoje</p>
</div>
</button>
<button onClick={() => setActiveTab('users')} className="w-full text-left p-3 rounded-xl bg-gray-50 hover:bg-gray-100 transition-colors flex items-center gap-3">
<div className="p-2 bg-blue-100 text-blue-600 rounded-lg"><Search size={18} /></div>
<div>
<p className="font-bold text-sm text-gray-800">Buscar Usuário</p>
<p className="text-xs text-gray-500">Ver detalhes de conta</p>
</div>
</button>
</div>
</div>
</div>
</div>
)}
{/* --- USERS TAB --- */}
{activeTab === 'users' && (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-200 flex gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" size={18} />
<input
type="text"
placeholder="Buscar por nome ou email..."
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500/50"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead className="bg-gray-50 text-gray-500 uppercase text-xs font-bold">
<tr>
<th className="px-6 py-4">Usuário</th>
<th className="px-6 py-4">Status</th>
<th className="px-6 py-4">Plano</th>
<th className="px-6 py-4">Início</th>
<th className="px-6 py-4">Término</th>
<th className="px-6 py-4">Pro?</th>
<th className="px-6 py-4">LTV</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{filteredUsers.length === 0 ? (
<tr>
<td colSpan={7} className="px-6 py-8 text-center text-gray-400">
Nenhum usuário encontrado na busca.
</td>
</tr>
) : filteredUsers.map((u) => (
<tr key={u.id} className="hover:bg-gray-50 transition-colors">
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-brand-100 text-brand-700 flex items-center justify-center font-bold text-xs">
{u.full_name ? u.full_name.substring(0, 2).toUpperCase() : 'US'}
</div>
<div>
<div className="font-bold text-gray-900">{u.full_name || 'Usuário Sem Nome'}</div>
<div className="text-xs text-gray-500">{u.email}</div>
</div>
</div>
</td>
<td className="px-6 py-4">
<StatusBadge status={u.plan_status} />
</td>
<td className="px-6 py-4">
<IntervalBadge interval={u.plan_interval} />
</td>
<td className="px-6 py-4 text-gray-600 font-medium text-xs">
{u.plan_start_date ? (
<span className="text-green-700 font-bold">{formatDate(u.plan_start_date)}</span>
) : (
<div className="flex items-center gap-1.5 text-gray-400" title="Data de Cadastro">
<Clock size={12} />
{formatDate(u.created_at)}
</div>
)}
</td>
<td className="px-6 py-4 text-gray-600 font-medium text-xs">
{u.plan_end_date ? formatDate(u.plan_end_date) : <span className="text-gray-300">-</span>}
</td>
<td className="px-6 py-4">
<button
onClick={() => handleToggleProfessional(u.id, !u.is_professional)}
className={`px-3 py-1 rounded-full text-xs font-bold transition-colors ${u.is_professional
? 'bg-purple-100 text-purple-700 hover:bg-purple-200'
: 'bg-gray-100 text-gray-400 hover:bg-gray-200'
}`}
>
{u.is_professional ? 'PRO' : 'ALUNO'}
</button>
</td>
<td className="px-6 py-4 font-mono font-medium text-gray-700">
{formatCurrency(u.lifetime_value || 0)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* --- COUPONS TAB --- */}
{activeTab === 'coupons' && (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="bg-gradient-to-r from-purple-600 to-indigo-600 rounded-2xl p-8 text-white flex flex-col md:flex-row items-center justify-between gap-6 shadow-lg">
<div>
<h2 className="text-2xl font-bold mb-2">Marketing & Ofertas</h2>
<p className="text-purple-100 opacity-90 max-w-lg">
Crie códigos promocionais para influenciadores, campanhas de email ou recuperação de carrinho.
</p>
</div>
<button
onClick={() => setShowCouponModal(true)}
className="bg-white text-purple-600 font-bold px-6 py-3 rounded-xl hover:bg-purple-50 transition-colors shadow-xl"
>
Criar Novo Cupom
</button>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{coupons.length === 0 ? (
<div className="col-span-full text-center py-12 text-gray-400 bg-white rounded-xl border border-dashed border-gray-300">
Nenhum cupom ativo no momento.
</div>
) : coupons.map(c => (
<div key={c.id} className="bg-white rounded-xl border border-gray-200 p-6 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden group">
<div className="absolute top-0 right-0 p-2 opacity-50">
<Ticket size={80} className="text-gray-100 -rotate-12 transform translate-x-4 -translate-y-4" />
</div>
<div className="relative z-10">
<div className="flex justify-between items-start mb-4">
<div className="bg-purple-50 text-purple-700 px-3 py-1 rounded-lg font-mono font-bold text-sm border border-purple-100">
{c.code}
</div>
<div className={`text-xs font-bold px-2 py-1 rounded-full ${c.is_active ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
{c.is_active ? 'ATIVO' : 'INATIVO'}
</div>
</div>
<div className="mb-4">
<span className="text-4xl font-black text-gray-900">{c.discount_percent}%</span>
<span className="text-sm text-gray-500 font-medium ml-1">OFF</span>
</div>
<div className="flex items-center gap-4 text-sm text-gray-500 border-t border-gray-100 pt-4">
<div className="flex items-center gap-1">
<Users size={14} />
<span>{c.uses_count} / {c.max_uses} usos</span>
</div>
<div className="flex items-center gap-1">
<Calendar size={14} />
<span>Criado em {new Date(c.created_at).toLocaleDateString()}</span>
</div>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* --- SETTINGS TAB --- */}
{activeTab === 'settings' && (
<div className="animate-in fade-in slide-in-from-bottom-4 duration-500 max-w-2xl">
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
<div className="p-6 border-b border-gray-100">
<h2 className="text-xl font-bold text-gray-900 flex items-center gap-2">
<Smartphone size={20} className="text-brand-600" />
Integração WhatsApp
</h2>
<p className="text-gray-500 text-sm mt-1">
Configure o número que receberá as mensagens e imagens dos usuários para análise.
</p>
</div>
<div className="p-6 space-y-6">
<form onSubmit={handleSaveSettings}>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Número do WhatsApp (Business/Bot)
</label>
<div className="flex gap-2">
<input
type="text"
required
className="flex-1 px-4 py-2 bg-white text-gray-900 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-500 placeholder-gray-400"
placeholder="Ex: 5541999999999"
value={config.whatsapp_number}
onChange={(e) => setConfig({ ...config, whatsapp_number: e.target.value.replace(/\D/g, '') })}
/>
<button
type="button"
className="bg-gray-100 text-gray-700 px-4 py-2 rounded-lg text-sm font-medium hover:bg-gray-200 transition-colors"
onClick={() => window.open(`https://wa.me/${config.whatsapp_number}`, '_blank')}
>
Testar Link
</button>
</div>
<p className="text-xs text-gray-500 mt-2">
Insira apenas números, incluindo o código do país (Ex: 55 para Brasil). Este número será usado para gerar o QR Code no painel do usuário.
</p>
</div>
<div className="pt-6 border-t border-gray-100 flex justify-end">
<button
type="submit"
disabled={savingConfig}
className="bg-brand-600 text-white px-6 py-2.5 rounded-lg font-bold hover:bg-brand-700 shadow-lg shadow-brand-500/20 flex items-center gap-2 disabled:opacity-50"
>
{savingConfig ? <div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-b-transparent"></div> : <Save size={18} />}
Salvar Configurações
</button>
</div>
</form>
</div>
</div>
</div>
)}
{/* --- FINANCIAL TAB --- */}
{activeTab === 'financial' && (
<div className="animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="bg-white rounded-xl border border-gray-200 p-8 text-center">
<div className="w-16 h-16 bg-blue-50 text-blue-600 rounded-2xl flex items-center justify-center mx-auto mb-4">
<CreditCard size={32} />
</div>
<h2 className="text-xl font-bold text-gray-900 mb-2">Integração Stripe Connect</h2>
<p className="text-gray-500 max-w-lg mx-auto mb-6">
Para visualizar o histórico detalhado de transações em tempo real, configure os Webhooks do Stripe no backend. O sistema atual está pronto para receber os dados na tabela <code>payments</code>.
</p>
<button className="text-blue-600 font-bold hover:underline flex items-center justify-center gap-2">
<ExternalLinkIcon /> Acessar Dashboard do Stripe
</button>
</div>
</div>
)}
</div>
)}
</main>
{/* Coupon Modal */}
{showCouponModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm animate-in fade-in duration-200">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md p-6 animate-in zoom-in-95 duration-200 border border-gray-100">
<h3 className="text-xl font-bold text-gray-900 mb-4">Criar Novo Cupom</h3>
<div className="mb-6 bg-blue-50 text-blue-800 p-4 rounded-xl border border-blue-100 text-sm flex gap-3">
<Info className="shrink-0 mt-0.5" size={18} />
<p>
<strong>Atenção:</strong> Ao criar o cupom aqui, você apenas registra para métricas internas. <br />
<span className="block mt-2 font-medium underline">Você deve criar o mesmo código de cupom no Dashboard do Stripe</span> para que o desconto funcione no checkout.
</p>
</div>
<form onSubmit={handleCreateCoupon} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Código do Cupom</label>
<input
type="text"
required
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-500 uppercase bg-white text-gray-900"
placeholder="EX: VERÃO2025"
value={newCoupon.code}
onChange={e => setNewCoupon({ ...newCoupon, code: e.target.value })}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Desconto (%)</label>
<input
type="number"
required
min="1" max="100"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-500 bg-white text-gray-900"
value={newCoupon.percent}
onChange={e => setNewCoupon({ ...newCoupon, percent: parseInt(e.target.value) })}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Limite de Usos</label>
<input
type="number"
required
min="1"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-500 bg-white text-gray-900"
value={newCoupon.uses}
onChange={e => setNewCoupon({ ...newCoupon, uses: parseInt(e.target.value) })}
/>
</div>
</div>
<div className="pt-4 flex gap-3">
<button
type="button"
onClick={() => setShowCouponModal(false)}
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg text-gray-700 font-medium hover:bg-gray-50"
>
Cancelar
</button>
<button
type="submit"
className="flex-1 px-4 py-2 bg-brand-600 text-white rounded-lg font-bold hover:bg-brand-700 shadow-lg shadow-brand-500/20"
>
Criar Cupom
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};
// UI Components
const NavButton = ({ active, onClick, icon, label }: any) => (
<button
onClick={onClick}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200 ${active ? 'bg-brand-600 text-white shadow-lg shadow-brand-900/20' : 'text-gray-400 hover:bg-gray-800 hover:text-white'}`}
>
{icon}
<span className="text-sm font-medium">{label}</span>
</button>
);
const KpiCard = ({ title, value, icon, color, trend }: any) => (
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200 flex items-center gap-4 relative overflow-hidden group">
<div className={`w-14 h-14 rounded-2xl ${color} flex items-center justify-center shadow-lg`}>
{icon}
</div>
<div>
<p className="text-gray-500 text-xs font-bold uppercase tracking-wider mb-1">{title}</p>
<h4 className="text-2xl font-black text-gray-900">{value}</h4>
{trend && (
<div className="flex items-center gap-1 mt-1 text-xs font-bold text-green-600 bg-green-50 px-2 py-0.5 rounded-full w-fit">
<TrendingUp size={12} /> {trend}
</div>
)}
</div>
</div>
);
const StatusBadge = ({ status }: { status: string }) => {
let styles = 'bg-gray-100 text-gray-600';
let icon = <MoreHorizontal size={12} />;
let label = status;
if (status === 'pro') {
styles = 'bg-green-100 text-green-700 border border-green-200';
icon = <CheckCircle2 size={12} />;
}
else if (status === 'trial') {
styles = 'bg-orange-100 text-orange-700 border border-orange-200';
icon = <Activity size={12} />;
}
else if (status === 'free' || !status) {
return (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold uppercase tracking-wide bg-gray-100 text-gray-600 border border-gray-200">
<User size={12} /> Gratuito
</span>
);
}
return (
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold uppercase tracking-wide ${styles}`}>
{icon} {label}
</span>
);
};
const IntervalBadge = ({ interval }: { interval: string }) => {
if (interval === 'free' || !interval) {
return <span className="px-2.5 py-1 rounded-lg text-xs font-bold bg-gray-50 text-gray-500 border border-gray-200">Básico</span>;
}
let color = 'bg-gray-100 text-gray-600';
let label = interval;
if (interval === 'monthly') { color = 'bg-blue-50 text-blue-700 border border-blue-100'; label = 'Mensal'; }
if (interval === 'quarterly') { color = 'bg-indigo-50 text-indigo-700 border border-indigo-100'; label = 'Trimestral'; }
if (interval === 'annual') { color = 'bg-purple-50 text-purple-700 border border-purple-100'; label = 'Anual'; }
return (
<span className={`px-2.5 py-1 rounded-lg text-xs font-bold ${color}`}>
{label}
</span>
);
};
const ExternalLinkIcon = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>
);
export default AdminPanel;