edumanagerpro2/manager/components/Settings.tsx

718 lines
34 KiB
TypeScript

import React, { useState, useMemo } from 'react';
import { SchoolData, SchoolProfile } from '../types';
import { dbService } from '../services/dbService';
import { Download, Upload, Trash2, Database, School, Camera, FileText, Info, AlertTriangle, X, CheckCircle, AlertCircle, Cloud, HelpCircle, RefreshCw, Plus, User } from 'lucide-react';
import { isSupabaseConfigured, uploadLogo } from '../services/supabase';
import { useDialog } from '../DialogContext';
import imageCompression from 'browser-image-compression';
interface SettingsProps {
data: SchoolData;
updateData: (newData: Partial<SchoolData>) => void;
setData: (data: SchoolData) => void;
}
const Settings: React.FC<SettingsProps> = ({ data, updateData, setData }) => {
const { showAlert, showConfirm } = useDialog();
const [selectedProfileId, setSelectedProfileId] = useState<string>(data.profile.id || 'main-school');
const [profiles, setProfiles] = useState<SchoolProfile[]>(data.profiles || [data.profile]);
const [globalLogo, setGlobalLogo] = useState<string>(data.logo || '');
const currentProfile = profiles.find(p => p.id === selectedProfileId) || profiles[0];
const currentDirector = useMemo(() => {
const employees = data.employees || [];
const categories = data.employeeCategories || [];
return employees.find(e => {
const cat = categories.find(c => c.id === e.categoryId);
const catName = cat?.name.toLowerCase() || '';
const empName = e.name.toLowerCase();
return catName.includes('diretor') || catName.includes('diretoria') ||
empName.includes('diretor') || empName.includes('diretoria');
});
}, [data.employees, data.employeeCategories]);
const [profileForm, setProfileForm] = useState<SchoolProfile>(currentProfile);
const [showEvolutionModal, setShowEvolutionModal] = useState(false);
const [evolutionForm, setEvolutionForm] = useState({
apiUrl: data.evolutionConfig?.apiUrl || '',
instanceName: data.evolutionConfig?.instanceName || '',
apiKey: data.evolutionConfig?.apiKey || ''
});
const saveEvolutionConfig = () => {
updateData({ evolutionConfig: evolutionForm });
setShowEvolutionModal(false);
showAlert('Sucesso', 'Configurações da Evolution API salvas!', 'success');
};
React.useEffect(() => {
setProfileForm(currentProfile);
}, [selectedProfileId, profiles]);
React.useEffect(() => {
setGlobalLogo(data.logo || '');
}, [data.logo]);
const [activeTab, setActiveTab] = useState<'perfil' | 'monitoramento'>('perfil');
const [apiLogs, setApiLogs] = useState<any[]>([]);
// Helper para normalizar URLs de fotos (vacina contra cache antigo)
const normalizePhotoUrl = (url?: string) => {
if (!url || typeof url !== 'string') return '';
if (url.startsWith('data:image')) return url;
if (url.startsWith('/storage/')) return url;
try {
const match = url.match(/^https?:\/\/[^\/]+\/(.+)$/);
if (match) return `/storage/${match[1]}`;
} catch(e) {}
return url;
};
const [systemStats, setSystemStats] = useState<any>(null);
const fetchStats = () => {
fetch('/api/system-stats')
.then(res => res.json())
.then(data => {
if (data.error) console.error('Erro na API:', data.error);
setSystemStats(data);
})
.catch(err => {
console.error('Erro ao buscar stats do sistema:', err);
setSystemStats({ error: true });
});
};
React.useEffect(() => {
fetchStats();
const interval = setInterval(fetchStats, 30000); // Atualiza a cada 30s
return () => clearInterval(interval);
}, []);
React.useEffect(() => {
if (activeTab === 'monitoramento') {
fetch('/api/logs')
.then(res => res.json())
.then(data => setApiLogs(data))
.catch(err => console.error('Erro ao buscar logs:', err));
}
}, [activeTab]);
const validateCNPJ = (cnpj: string) => {
cnpj = cnpj.replace(/[^\d]+/g, '');
if (cnpj === '' || cnpj.length !== 14) return false;
if (/^(\d)\1+$/.test(cnpj)) return false;
let tamanho = cnpj.length - 2;
let numeros = cnpj.substring(0, tamanho);
let digitos = cnpj.substring(tamanho);
let soma = 0;
let pos = tamanho - 7;
for (let i = tamanho; i >= 1; i--) {
soma += parseInt(numeros.charAt(tamanho - i)) * pos--;
if (pos < 2) pos = 9;
}
let resultado = soma % 11 < 2 ? 0 : 11 - (soma % 11);
if (resultado !== parseInt(digitos.charAt(0))) return false;
tamanho = tamanho + 1;
numeros = cnpj.substring(0, tamanho);
soma = 0;
pos = tamanho - 7;
for (let i = tamanho; i >= 1; i--) {
soma += parseInt(numeros.charAt(tamanho - i)) * pos--;
if (pos < 2) pos = 9;
}
resultado = soma % 11 < 2 ? 0 : 11 - (soma % 11);
if (resultado !== parseInt(digitos.charAt(1))) return false;
return true;
};
const handleZipChange = async (zip: string) => {
const cleanZip = zip.replace(/\D/g, '');
setProfileForm(prev => ({ ...prev, zip: zip.replace(/^(\d{5})(\d)/, '$1-$2').slice(0, 9) }));
if (cleanZip.length === 8) {
try {
const response = await fetch(`https://viacep.com.br/ws/${cleanZip}/json/`);
const data = await response.json();
if (!data.erro) {
setProfileForm(prev => ({
...prev,
address: data.logradouro,
city: data.localidade,
state: data.uf
}));
}
} catch (error) {
console.error('Erro ao buscar CEP:', error);
}
}
};
const saveProfile = () => {
if (!validateCNPJ(profileForm.cnpj)) {
showAlert('Erro', 'CNPJ inválido. Por favor, insira um CNPJ verdadeiro.', 'error');
return;
}
// Check if trying to set as Matriz but another Matriz already exists
if (profileForm.type === 'matriz') {
const otherMatriz = profiles.find(p => p.type === 'matriz' && p.id !== profileForm.id);
if (otherMatriz) {
showAlert('Erro', `Já existe uma matriz cadastrada (${otherMatriz.name}). Só é permitida uma matriz.`, 'error');
return;
}
}
const updatedProfiles = profiles.map(p => p.id === profileForm.id ? profileForm : p);
const mainProfile = updatedProfiles.find(p => p.type === 'matriz') || updatedProfiles[0];
setProfiles(updatedProfiles);
updateData({ profiles: updatedProfiles, profile: mainProfile });
showAlert('Sucesso', 'Configurações salvas com sucesso!', 'success');
};
const addNewInstitution = () => {
const newId = `school-${Date.now()}`;
const newProfile: SchoolProfile = {
id: newId,
name: 'Nova Instituição',
address: '',
city: '',
state: '',
zip: '',
cnpj: '',
phone: '',
email: '',
type: 'filial'
};
setProfiles([...profiles, newProfile]);
setSelectedProfileId(newId);
};
const deleteInstitution = (id: string) => {
if (profiles.length <= 1) {
showAlert('Erro', 'É necessário ter pelo menos uma instituição cadastrada.', 'error');
return;
}
const profileToDelete = profiles.find(p => p.id === id);
if (profileToDelete?.type === 'matriz') {
showAlert('Erro', 'Não é possível excluir a instituição matriz. Altere outra para matriz primeiro.', 'error');
return;
}
showConfirm(
'Excluir Instituição?',
`Tem certeza que deseja excluir a instituição "${profileToDelete?.name}"?`,
() => {
const updatedProfiles = profiles.filter(p => p.id !== id);
setProfiles(updatedProfiles);
setSelectedProfileId(updatedProfiles[0].id);
updateData({ profiles: updatedProfiles, profile: updatedProfiles[0] });
}
);
};
const closeModal = () => {
setIsClosing(true);
setTimeout(() => {
setShowImportModal(false);
setIsClosing(false);
}, 300);
};
const handleLogoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
try {
showAlert('Aguarde', 'Fazendo upload e otimizando a logo...', 'info');
// Compression options
const options = {
maxSizeMB: 0.1, // 100KB
maxWidthOrHeight: 500,
useWebWorker: true
};
const compressedFile = await imageCompression(file, options);
const url = await uploadLogo(compressedFile);
if (!url) {
throw new Error("Falha ao obter a URL da logo após o upload");
}
setGlobalLogo(url);
updateData({ logo: url });
showAlert('Sucesso', 'Logo atualizada com sucesso!', 'success');
} catch (error) {
console.error('Erro ao fazer upload da imagem:', error);
showAlert('Erro', 'Falha ao processar e salvar a imagem.', 'error');
}
}
};
const handleReset = async () => {
await dbService.resetData();
window.location.reload();
};
const formatPhone = (value: string) => {
return value
.replace(/\D/g, '')
.replace(/^(\d{2})(\d)/, '($1) $2 ')
.replace(/(\d{4})(\d)/, '$1-$2')
.slice(0, 16);
};
const inputClass = "w-full px-4 py-3 bg-white text-black border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-all shadow-sm text-sm";
return (
<div className="space-y-8 animate-in fade-in duration-300 pb-20">
<header>
<h2 className="text-3xl font-extrabold text-slate-900 tracking-tight">Configurações</h2>
<p className="text-slate-500 font-medium">Gerencie o perfil da escola, modelo de contrato e dados.</p>
<div className="flex gap-4 mt-6 border-b border-slate-200">
<button
onClick={() => setActiveTab('perfil')}
className={`pb-2 font-bold text-sm ${activeTab === 'perfil' ? 'text-indigo-600 border-b-2 border-indigo-600' : 'text-slate-500 hover:text-slate-700'}`}
>
Perfil
</button>
<button
onClick={() => setActiveTab('monitoramento')}
className={`pb-2 font-bold text-sm ${activeTab === 'monitoramento' ? 'text-indigo-600 border-b-2 border-indigo-600' : 'text-slate-500 hover:text-slate-700'}`}
>
Monitoramento de API
</button>
</div>
</header>
{activeTab === 'perfil' ? (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2 space-y-6">
<div className="bg-white p-8 rounded-xl border border-slate-200 shadow-xl space-y-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3 text-indigo-600">
<div className="p-3 bg-indigo-50 rounded-lg">
<School size={24} />
</div>
<h3 className="text-xl font-black text-slate-800">Perfil da Instituição</h3>
</div>
<button
onClick={addNewInstitution}
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-all font-bold text-xs shadow-md"
>
<Plus size={16} /> Nova Instituição
</button>
</div>
{/* Institution Selector */}
<div className="flex flex-wrap gap-2 mb-6">
{profiles.map(p => (
<div key={p.id} className="flex items-center">
<button
onClick={() => setSelectedProfileId(p.id)}
className={`px-4 py-2 rounded-lg font-bold text-xs transition-all border ${
selectedProfileId === p.id
? 'bg-indigo-600 text-white border-indigo-600 shadow-md'
: 'bg-white text-slate-600 border-slate-200 hover:border-indigo-300'
}`}
>
{p.name} {p.type === 'matriz' && '(Matriz)'}
</button>
{p.id !== selectedProfileId && p.type !== 'matriz' && (
<button
onClick={(e) => { e.stopPropagation(); deleteInstitution(p.id); }}
className="ml-1 p-1 text-red-400 hover:text-red-600 transition-colors"
>
<Trash2 size={14} />
</button>
)}
</div>
))}
</div>
<div className="flex flex-col md:flex-row gap-8">
<div className="flex flex-col items-center gap-4">
<div className="w-40 h-40 rounded-xl bg-slate-50 border-2 border-dashed border-slate-200 flex items-center justify-center overflow-hidden relative group shadow-inner">
{globalLogo ? (
<div className="w-full h-full bg-slate-50 flex items-center justify-center p-4">
<img src={normalizePhotoUrl(globalLogo)} alt="Logo" className="w-full h-full object-contain p-2" />
</div>
) : (
<div className="text-slate-300 text-center p-4">
<Camera size={40} className="mx-auto mb-2 opacity-20" />
<span className="text-[10px] font-bold uppercase text-slate-500">Logo Global</span>
</div>
)}
<label className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center cursor-pointer text-white">
<Upload size={24} />
<input type="file" accept="image/*" className="hidden" onChange={handleLogoUpload} />
</label>
</div>
<p className="text-[10px] font-bold text-slate-400 uppercase text-center">Logo única para todas as unidades</p>
</div>
<div className="flex-1 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1.5 ml-1">Nome da Escola</label>
<input className={inputClass} value={profileForm.name} onChange={e => setProfileForm({...profileForm, name: e.target.value})} />
</div>
<div>
<label className="block text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1.5 ml-1">CNPJ</label>
<input className={inputClass} placeholder="00.000.000/0001-00" value={profileForm.cnpj} onChange={e => setProfileForm({...profileForm, cnpj: e.target.value})} />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="md:col-span-1">
<label className="block text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1.5 ml-1">CEP</label>
<input className={inputClass} placeholder="00000-000" value={profileForm.zip} onChange={e => handleZipChange(e.target.value)} />
</div>
<div className="md:col-span-2">
<label className="block text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1.5 ml-1">Endereço</label>
<input className={inputClass} value={profileForm.address} onChange={e => setProfileForm({...profileForm, address: e.target.value})} />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="md:col-span-1">
<label className="block text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1.5 ml-1">Cidade</label>
<input className={inputClass} value={profileForm.city} onChange={e => setProfileForm({...profileForm, city: e.target.value})} />
</div>
<div className="md:col-span-1">
<label className="block text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1.5 ml-1">Estado (UF)</label>
<input className={inputClass} placeholder="UF" value={profileForm.state} onChange={e => setProfileForm({...profileForm, state: e.target.value.toUpperCase().slice(0, 2)})} />
</div>
<div className="md:col-span-1">
<label className="block text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1.5 ml-1">Tipo</label>
<select
className={inputClass}
value={profileForm.type}
onChange={e => setProfileForm({...profileForm, type: e.target.value as 'matriz' | 'filial'})}
>
<option value="matriz">Matriz</option>
<option value="filial">Filial</option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1.5 ml-1">Telefone</label>
<input className={inputClass} placeholder="(00) 0 0000-0000" value={profileForm.phone} onChange={e => setProfileForm({...profileForm, phone: formatPhone(e.target.value)})} maxLength={16} />
</div>
<div>
<label className="block text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1.5 ml-1">Email</label>
<input className={inputClass} placeholder="Email" value={profileForm.email} onChange={e => setProfileForm({...profileForm, email: e.target.value})} />
</div>
</div>
</div>
</div>
<div className="pt-4">
<button
onClick={saveProfile}
className="w-full py-4 bg-indigo-600 text-white rounded-xl hover:bg-indigo-700 transition-all shadow-lg font-bold text-sm"
>
Salvar Perfil da Instituição
</button>
</div>
</div>
</div>
<div className="space-y-6">
{/* POSTGRESQL CARD */}
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-xl space-y-4 relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-50 rounded-full -mr-16 -mt-16 pointer-events-none"></div>
<div className="flex items-center justify-between relative z-10">
<div className="flex items-center gap-3 text-blue-600">
<div className="p-2 bg-blue-50 rounded-lg shadow-sm border border-blue-100">
<Database size={20} />
</div>
<h3 className="text-lg font-black text-slate-800">Banco de Dados</h3>
</div>
{systemStats ? (
<span className="flex items-center gap-1.5 px-2.5 py-1 bg-emerald-100 text-emerald-700 rounded-full text-[10px] font-black uppercase tracking-wider">
<CheckCircle size={12} /> Online
</span>
) : (
<RefreshCw size={16} className="text-slate-300 animate-spin" />
)}
</div>
<div className="grid grid-cols-2 gap-3 mt-4 relative z-10">
<div className="p-3 bg-slate-50 rounded-xl border border-slate-100 shadow-inner">
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1">Tamanho em Disco</p>
<p className="text-xl font-black text-slate-800">{systemStats?.postgres?.dbSize || '--'}</p>
</div>
<div className="p-3 bg-slate-50 rounded-xl border border-slate-100 shadow-inner">
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1">Tabelas SGBD</p>
<p className="text-xl font-black text-slate-800">{systemStats?.postgres?.tableCount || '--'} <span className="text-sm font-medium text-slate-400">PostgreSQL</span></p>
</div>
</div>
</div>
{/* MINIO STORAGE CARD */}
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-xl space-y-4 relative overflow-hidden">
<div className="absolute bottom-0 right-0 w-24 h-24 bg-red-50 rounded-full -mr-8 -mb-8 pointer-events-none"></div>
<div className="flex items-center justify-between relative z-10">
<div className="flex items-center gap-3 text-red-600">
<div className="p-2 bg-red-50 rounded-lg shadow-sm border border-red-100">
<Cloud size={20} />
</div>
<h3 className="text-lg font-black text-slate-800">Storage Físico</h3>
</div>
{systemStats && !systemStats.minio?.error ? (
<span className="flex items-center gap-1.5 px-2.5 py-1 bg-emerald-100 text-emerald-700 rounded-full text-[10px] font-black uppercase tracking-wider">
<CheckCircle size={12} /> MinIO
</span>
) : (
<span className="flex items-center gap-1.5 px-2.5 py-1 bg-red-100 text-red-700 rounded-full text-[10px] font-black uppercase tracking-wider">
<AlertTriangle size={12} /> Backup
</span>
)}
</div>
<div className="flex gap-4 relative z-10">
<div className="flex-1 p-3 bg-slate-50 rounded-xl border border-slate-100 shadow-inner">
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1">Uso Total</p>
<p className="text-xl font-black text-slate-800">{systemStats?.minio?.totalSizeMB || '0.00'} <span className="text-sm font-medium text-slate-400">MB</span></p>
</div>
<div className="flex-1 p-3 bg-slate-50 rounded-xl border border-slate-100 shadow-inner">
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-1">Arquivos</p>
<p className="text-xl font-black text-slate-800">{systemStats?.minio?.totalItems || '0'}</p>
</div>
</div>
{systemStats?.minio?.buckets && systemStats.minio.buckets.length > 0 && (
<div className="pt-4 border-t border-slate-100 mt-2 relative z-10">
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-widest mb-3">Buckets Mapeados</p>
<div className="space-y-2">
{systemStats.minio.buckets.map((b: any, idx: number) => (
<div key={idx} className="flex items-center justify-between bg-white p-3 rounded-lg border border-slate-100 shadow-sm hover:border-red-200 transition-colors">
<div className="flex items-center gap-3">
<div className="w-2.5 h-2.5 rounded-full bg-red-500 shadow-sm shadow-red-200"></div>
<span className="text-sm font-bold text-slate-700">{b.name}</span>
</div>
<div className="text-xs font-bold text-slate-400">
<span className="text-slate-600">{b.items}</span> itens <span className="text-slate-600">{b.sizeMB}</span> MB
</div>
</div>
))}
</div>
</div>
)}
</div>
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-xl space-y-4">
<div className="flex items-center gap-3 text-indigo-600">
<div className="p-2 bg-indigo-50 rounded-lg">
<Database size={20} />
</div>
<h3 className="text-lg font-black text-slate-800">Dados do System</h3>
</div>
<button onClick={async () => await dbService.exportData()} className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-slate-800 text-white rounded-lg hover:bg-slate-700 transition-all font-bold text-xs">
<Download size={16} /> Exportar Backup
</button>
<label className="flex items-center justify-center gap-2 px-4 py-3 border-2 border-dashed border-slate-200 text-slate-600 rounded-lg cursor-pointer hover:bg-slate-50 transition-colors font-bold text-xs">
<Upload size={16} /> Importar Backup
<input type="file" className="hidden" accept=".json" onChange={async (e) => {
const file = e.target.files?.[0];
if (file) {
showConfirm(
'Substituir Dados?',
'⚠️ Tem certeza que deseja substituir todos os dados atuais? Esta ação não pode ser desfeita.',
async () => {
await dbService.importData(file);
const newData = await dbService.initData();
setData(newData);
showAlert('Sucesso', '✅ Dados restaurados com sucesso!', 'success');
}
);
}
}} />
</label>
</div>
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-xl space-y-4">
<div className="flex items-center gap-3 text-indigo-600">
<div className="p-2 bg-indigo-50 rounded-lg">
<FileText size={20} />
</div>
<h3 className="text-lg font-black text-slate-800">Evolution API</h3>
</div>
<div className="p-4 rounded-lg bg-slate-50 border border-slate-200">
{data.evolutionConfig?.apiUrl ? (
<div className="space-y-2 text-sm text-slate-600">
<p><strong>URL:</strong> {data.evolutionConfig.apiUrl}</p>
<p><strong>Instância:</strong> {data.evolutionConfig.instanceName}</p>
<p><strong>API Key:</strong> </p>
</div>
) : (
<p className="text-xs text-slate-500 text-center">Nenhuma credencial configurada.</p>
)}
</div>
<button
onClick={() => setShowEvolutionModal(true)}
className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-all font-bold text-xs shadow-md"
>
<Plus size={16} /> Configurar Credenciais
</button>
</div>
<div className="bg-gradient-to-br from-indigo-50 to-blue-50 p-6 rounded-xl border border-indigo-100 shadow-xl space-y-4">
<div className="flex items-center gap-3 text-indigo-800">
<div className="p-2 bg-white rounded-lg shadow-sm">
<User size={20} className="text-indigo-600" />
</div>
<h3 className="text-lg font-black text-slate-800">Responsável Legal / Diretor</h3>
</div>
<div className="p-4 rounded-lg bg-white border border-indigo-50 shadow-sm">
{currentDirector ? (
<div className="space-y-2 text-sm text-slate-700">
<p><strong>Nome:</strong> {currentDirector.name}</p>
<p><strong>CPF:</strong> {currentDirector.cpf}</p>
<p className="text-xs text-indigo-500 mt-2 font-medium bg-indigo-50 inline-block px-2 py-1 rounded">Este responsável assinará automaticamente os documentos.</p>
</div>
) : (
<p className="text-xs text-slate-500 text-center">Nenhum diretor localizado. Cadastre um funcionário como Diretor na aba Funcionários.</p>
)}
</div>
</div>
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-xl">
<button
onClick={() => showConfirm(
'Resetar Sistema',
'Isso apagará TODOS os dados cadastrados. Não há como desfazer.',
handleReset,
'alert'
)}
className="w-full py-3 border border-red-200 text-red-600 rounded-lg hover:bg-red-50 transition-colors font-bold text-xs flex items-center justify-center gap-2"
>
<Trash2 size={16} /> Resetar Fábrica
</button>
</div>
</div>
</div>
) : (
<div className="bg-white p-8 rounded-xl border border-slate-200 shadow-xl">
<h3 className="text-xl font-black text-slate-800 mb-6">Logs de API</h3>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead className="bg-slate-50 text-slate-500 uppercase text-[10px] font-black tracking-wider">
<tr>
<th className="px-4 py-3">Data</th>
<th className="px-4 py-3">Serviço</th>
<th className="px-4 py-3">Ação</th>
<th className="px-4 py-3">Detalhes</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{apiLogs.map((log, i) => (
<tr key={i}>
<td className="px-4 py-3 text-slate-500">{new Date(log.date).toLocaleString()}</td>
<td className="px-4 py-3 font-bold text-indigo-600">{log.service}</td>
<td className="px-4 py-3 text-slate-700">{log.action}</td>
<td className="px-4 py-3 text-slate-600 text-xs font-mono">{JSON.stringify(log.details)}</td>
</tr>
))}
{apiLogs.length === 0 && (
<tr>
<td colSpan={4} className="px-4 py-8 text-center text-slate-400">Nenhum log encontrado.</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
)}
{/* Evolution API Modal */}
{showEvolutionModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-transparent animate-in fade-in duration-200">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden animate-slide-up">
<div className="px-6 py-5 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
<h3 className="text-xl font-bold text-slate-800">Credenciais Evolution API</h3>
<button
onClick={() => setShowEvolutionModal(false)}
className="p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-full transition-colors"
>
<X size={20} />
</button>
</div>
<div className="p-6 space-y-4">
<div>
<label className="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">URL da API</label>
<input
type="text"
value={evolutionForm.apiUrl}
onChange={e => setEvolutionForm({...evolutionForm, apiUrl: e.target.value})}
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:bg-white transition-all shadow-sm text-sm"
placeholder="https://api.evolution.com"
/>
</div>
<div>
<label className="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">Nome da Instância</label>
<input
type="text"
value={evolutionForm.instanceName}
onChange={e => setEvolutionForm({...evolutionForm, instanceName: e.target.value})}
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:bg-white transition-all shadow-sm text-sm"
placeholder="minha-instancia"
/>
</div>
<div>
<label className="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">API Key</label>
<input
type="password"
value={evolutionForm.apiKey}
onChange={e => setEvolutionForm({...evolutionForm, apiKey: e.target.value})}
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:bg-white transition-all shadow-sm text-sm"
placeholder="••••••••••••"
/>
</div>
</div>
<div className="p-6 border-t border-slate-100 bg-slate-50/50 flex justify-end gap-3">
<button
onClick={() => setShowEvolutionModal(false)}
className="px-5 py-2.5 text-slate-600 font-semibold hover:bg-slate-200 rounded-xl transition-all"
>
Cancelar
</button>
<button
onClick={saveEvolutionConfig}
className="px-6 py-2.5 bg-indigo-600 text-white font-bold rounded-xl hover:bg-indigo-700 shadow-md transition-all flex items-center gap-2"
>
<CheckCircle size={18} /> Confirmar
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default Settings;