edumanagerpro2/manager/components/Settings.tsx

976 lines
48 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, Folder, File as FileIcon, Eye, ExternalLink, Image as ImageIcon, List } 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);
// Storage Explorer State
const [showStorageManagerModal, setShowStorageManagerModal] = useState(false);
const [selectedStorageBucket, setSelectedStorageBucket] = useState<string | null>(null);
const [storageObjects, setStorageObjects] = useState<any[]>([]);
const [loadingBucket, setLoadingBucket] = useState(false);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
// Database Explorer State
const [showDatabaseExplorerModal, setShowDatabaseExplorerModal] = useState(false);
const [dbTables, setDbTables] = useState<any[]>([]);
const [loadingDbTables, setLoadingDbTables] = useState(false);
const openDatabaseExplorer = async () => {
setShowDatabaseExplorerModal(true);
setLoadingDbTables(true);
try {
const res = await fetch('/api/database/tables');
const data = await res.json();
setDbTables(data.tables || []);
} catch (e) {
console.error(e);
showAlert('Erro', 'Não foi possível carregar as tabelas.', 'error');
} finally {
setLoadingDbTables(false);
}
};
const openBucket = async (bucketName: string) => {
setSelectedStorageBucket(bucketName);
setLoadingBucket(true);
try {
const res = await fetch(`/api/storage/buckets/${bucketName}/objects`);
const data = await res.json();
setStorageObjects(data.objects || []);
} catch (e) {
console.error(e);
showAlert('Erro', 'Não foi possível carregar os arquivos.', 'error');
} finally {
setLoadingBucket(false);
}
};
const deleteStorageObject = (bucket: string, key: string) => {
showConfirm('Excluir Arquivo', `Apagar permanentemente: ${key}?`, async () => {
try {
const res = await fetch(`/api/storage/buckets/${bucket}/objects`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key })
});
if (res.ok) {
setStorageObjects(prev => prev.filter(o => o.key !== key));
showAlert('Sucesso', 'Arquivo removido do disco físico.', 'success');
fetchStats(); // Update numbers
} else {
showAlert('Erro', 'Falha ao excluir arquivo.', 'error');
}
} catch (e) {
console.error(e);
}
});
};
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 className="pt-4 border-t border-slate-100 mt-4 relative z-10">
<button
onClick={openDatabaseExplorer}
className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-blue-50 text-blue-600 rounded-xl hover:bg-blue-100 transition-all font-black text-sm shadow-sm hover:-translate-y-0.5"
>
<List size={18} /> Explorar Estrutura de Dados
</button>
</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>
<div className="pt-4 border-t border-slate-100 mt-4 relative z-10">
<button
onClick={() => setShowStorageManagerModal(true)}
className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-red-50 text-red-600 rounded-xl hover:bg-red-100 transition-all font-black text-sm shadow-sm hover:-translate-y-0.5"
>
<Folder size={18} /> Abrir Gerenciador de Arquivos
</button>
</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>
)}
{/* Storage Explorer Modal */}
{showStorageManagerModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-transparent animate-in fade-in duration-300 pointer-events-auto">
<div className="bg-white rounded-3xl shadow-[0_0_50px_rgba(0,0,0,0.15)] w-full max-w-5xl h-[85vh] flex flex-col overflow-hidden animate-slide-up border border-slate-100">
{/* Header */}
<div className="px-8 py-6 border-b border-slate-200/50 flex items-center justify-between bg-white/50">
<div className="flex items-center gap-4 text-slate-800">
{selectedStorageBucket ? (
<button onClick={() => setSelectedStorageBucket(null)} className="p-3 bg-red-50 text-red-600 hover:bg-red-100 rounded-2xl shadow-sm transition-all" title="Voltar para Pastas">
<Cloud size={24} />
</button>
) : (
<div className="p-3 bg-red-100 text-red-600 rounded-2xl shadow-sm">
<Cloud size={28} />
</div>
)}
<div>
<h3 className="text-2xl font-black tracking-tight">{selectedStorageBucket ? selectedStorageBucket : 'Gerenciador de Arquivos'}</h3>
<p className="text-sm font-bold text-slate-500">
{selectedStorageBucket ? `${storageObjects.length} arquivos encontrados.` : `${systemStats?.minio?.buckets?.length || 0} pastas na nuvem.`}
</p>
</div>
</div>
<button
onClick={() => { setShowStorageManagerModal(false); setSelectedStorageBucket(null); }}
className="p-3 bg-white text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-2xl shadow-sm transition-all"
>
<X size={24} />
</button>
</div>
{/* Content Body */}
<div className="flex-1 overflow-y-auto p-8 bg-slate-50/30">
{!selectedStorageBucket ? (
// Pastas (Buckets) View
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{systemStats?.minio?.buckets?.map((b: any, idx: number) => (
<div key={idx} onClick={() => openBucket(b.name)} className="group bg-white p-6 rounded-2xl border border-slate-200 shadow-sm hover:border-red-300 hover:shadow-xl transition-all cursor-pointer flex items-center gap-5 hover:-translate-y-1">
<div className="p-4 bg-red-50 text-red-500 rounded-2xl group-hover:bg-red-500 group-hover:text-white transition-colors">
<Folder size={32} />
</div>
<div>
<h4 className="text-lg font-black text-slate-800">{b.name}</h4>
<p className="text-sm font-bold text-slate-400 mt-1">{b.items} arquivos {b.sizeMB} MB</p>
</div>
</div>
))}
{(!systemStats?.minio?.buckets || systemStats.minio.buckets.length === 0) && (
<div className="col-span-full py-10 text-center text-slate-400">
<Folder size={64} className="mx-auto mb-4 opacity-20" />
<p className="font-bold text-xl">Nenhuma pasta encontrada.</p>
</div>
)}
</div>
) : loadingBucket ? (
<div className="flex flex-col items-center justify-center h-full text-slate-400">
<RefreshCw size={48} className="animate-spin mb-4 text-red-400" />
<p className="font-bold">Acessando MinIO Storage...</p>
</div>
) : storageObjects.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-slate-400">
<Folder size={64} className="mb-4 opacity-20" />
<p className="font-bold text-xl">Pasta Vazia</p>
<p className="text-sm">Nenhum arquivo encontrado neste bucket.</p>
</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
{storageObjects.map((obj, i) => {
const isImage = obj.key.match(/\.(jpeg|jpg|gif|png|webp)$/i);
const isPdf = obj.key.match(/\.pdf$/i);
const sizeKB = (obj.size / 1024).toFixed(1);
return (
<div key={i} className="group bg-white rounded-2xl border border-slate-200/60 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all overflow-hidden flex flex-col">
{/* Thumbnail Area */}
<div className="h-32 bg-slate-100 relative flex items-center justify-center overflow-hidden border-b border-slate-100">
{isImage ? (
<img src={obj.url} alt={obj.key} className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500" loading="lazy" />
) : isPdf ? (
<FileText size={48} className="text-red-400/50 group-hover:scale-110 transition-transform" />
) : (
<FileIcon size={48} className="text-slate-300 group-hover:scale-110 transition-transform" />
)}
{/* Hover Actions Overlay */}
<div className="absolute inset-0 bg-slate-900/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-3 backdrop-blur-sm">
{(isImage || isPdf) && (
<button onClick={() => setPreviewUrl(obj.url)} className="p-2.5 bg-white/20 hover:bg-white text-white hover:text-slate-900 rounded-full transition-all" title="Visualizar">
<Eye size={18} />
</button>
)}
<a href={obj.url} download={obj.key} target="_blank" rel="noreferrer" className="p-2.5 bg-white/20 hover:bg-white text-white hover:text-indigo-600 rounded-full transition-all" title="Baixar Original">
<Download size={18} />
</a>
</div>
</div>
{/* File Info */}
<div className="p-4 flex-1 flex flex-col">
<p className="text-xs font-black text-slate-700 truncate" title={obj.key}>{obj.key.split('/').pop()}</p>
<div className="mt-auto pt-3 flex items-center justify-between">
<span className="text-[10px] font-bold text-slate-400 uppercase bg-slate-100 px-2 py-1 rounded-md">{sizeKB} KB</span>
<button onClick={() => deleteStorageObject(selectedStorageBucket, obj.key)} className="text-slate-300 hover:text-red-500 transition-colors" title="Excluir Permanentemente">
<Trash2 size={16} />
</button>
</div>
</div>
</div>
);
})}
</div>
)}
</div>
</div>
</div>
)}
{/* Database Explorer Modal */}
{showDatabaseExplorerModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-transparent animate-in fade-in duration-300 pointer-events-auto">
<div className="bg-white rounded-3xl shadow-[0_0_50px_rgba(0,0,0,0.15)] w-full max-w-5xl h-[85vh] flex flex-col overflow-hidden animate-slide-up border border-slate-100">
{/* Header */}
<div className="px-8 py-6 border-b border-slate-200/50 flex items-center justify-between bg-white/50">
<div className="flex items-center gap-4 text-slate-800">
<div className="p-3 bg-blue-100 text-blue-600 rounded-2xl shadow-sm">
<Database size={28} />
</div>
<div>
<h3 className="text-2xl font-black tracking-tight">Database Explorer</h3>
<p className="text-sm font-bold text-slate-500">
{dbTables.length} tabelas no schema public do PostgreSQL.
</p>
</div>
</div>
<button
onClick={() => setShowDatabaseExplorerModal(false)}
className="p-3 bg-white text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-2xl shadow-sm transition-all"
>
<X size={24} />
</button>
</div>
{/* Content Body */}
<div className="flex-1 overflow-y-auto p-8 bg-slate-50/30">
{loadingDbTables ? (
<div className="flex flex-col items-center justify-center h-full text-slate-400">
<RefreshCw size={48} className="animate-spin mb-4 text-blue-400" />
<p className="font-bold">Analisando Estrutura do PostgreSQL...</p>
</div>
) : dbTables.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-slate-400">
<Database size={64} className="mb-4 opacity-20" />
<p className="font-bold text-xl">Nenhuma tabela encontrada</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{dbTables.map((table, idx) => (
<div key={idx} className="group bg-white p-6 rounded-2xl border border-slate-200 shadow-sm hover:border-blue-300 hover:shadow-xl transition-all cursor-default flex items-center justify-between hover:-translate-y-1">
<div className="flex items-center gap-4">
<div className="p-3 bg-blue-50 text-blue-500 rounded-xl group-hover:bg-blue-500 group-hover:text-white transition-colors">
<List size={24} />
</div>
<div>
<h4 className="text-sm font-black text-slate-800 uppercase tracking-wide">{table.table_name}</h4>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs font-bold text-slate-500">{table.row_count} registros</span>
</div>
</div>
</div>
<span className="text-[10px] font-bold text-slate-400 uppercase bg-slate-100 px-2 py-1 rounded-md">{table.total_size}</span>
</div>
))}
</div>
)}
</div>
</div>
</div>
)}
{/* Lightbox Preview */}
{previewUrl && (
<div className="fixed inset-0 z-[60] bg-transparent flex items-center justify-center animate-in fade-in pointer-events-auto">
<button onClick={() => setPreviewUrl(null)} className="absolute top-6 right-6 p-3 bg-red-50 text-red-500 hover:bg-red-500 hover:text-white shadow-lg rounded-full transition-colors z-[61]">
<X size={24} />
</button>
<div className="bg-white p-4 rounded-2xl shadow-[0_0_50px_rgba(0,0,0,0.2)] border border-slate-100">
{previewUrl.match(/\.(jpeg|jpg|gif|png|webp)$/i) ? (
<img src={previewUrl} alt="Preview" className="max-w-[85vw] max-h-[85vh] object-contain rounded-lg animate-in zoom-in-95" />
) : (
<iframe src={previewUrl} className="w-[85vw] h-[85vh] rounded-lg animate-in zoom-in-95" title="PDF Preview" />
)}
</div>
</div>
)}
</div>
);
};
export default Settings;