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) => void; setData: (data: SchoolData) => void; } const Settings: React.FC = ({ data, updateData, setData }) => { const { showAlert, showConfirm } = useDialog(); const [selectedProfileId, setSelectedProfileId] = useState(data.profile.id || 'main-school'); const [profiles, setProfiles] = useState(data.profiles || [data.profile]); const [globalLogo, setGlobalLogo] = useState(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(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([]); // 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(null); // Storage Explorer State const [showStorageManagerModal, setShowStorageManagerModal] = useState(false); const [selectedStorageBucket, setSelectedStorageBucket] = useState(null); const [storageObjects, setStorageObjects] = useState([]); const [loadingBucket, setLoadingBucket] = useState(false); const [previewUrl, setPreviewUrl] = useState(null); // Database Explorer State const [showDatabaseExplorerModal, setShowDatabaseExplorerModal] = useState(false); const [dbTables, setDbTables] = useState([]); const [loadingDbTables, setLoadingDbTables] = useState(false); const [selectedDbTable, setSelectedDbTable] = useState(null); const [tableData, setTableData] = useState<{rows: any[], fields: string[]}>({rows: [], fields: []}); const [loadingTableData, setLoadingTableData] = useState(false); const openDatabaseExplorer = async () => { setShowDatabaseExplorerModal(true); setLoadingDbTables(true); setSelectedDbTable(null); 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 openTable = async (tableName: string) => { setSelectedDbTable(tableName); setLoadingTableData(true); try { const res = await fetch(`/api/database/tables/${tableName}/data`); const data = await res.json(); setTableData({ rows: data.rows || [], fields: data.fields || [] }); } catch (e) { console.error(e); showAlert('Erro', 'Não foi possível carregar os dados da tabela.', 'error'); } finally { setLoadingTableData(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 handleLogoUpload = async (e: React.ChangeEvent) => { 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 (

Configurações

Gerencie o perfil da escola, modelo de contrato e dados.

{activeTab === 'perfil' ? (

Perfil da Instituição

{/* Institution Selector */}
{profiles.map(p => (
{p.id !== selectedProfileId && p.type !== 'matriz' && ( )}
))}
{globalLogo ? (
Logo
) : (
Logo Global
)}

Logo única para todas as unidades

setProfileForm({...profileForm, name: e.target.value})} />
setProfileForm({...profileForm, cnpj: e.target.value})} />
handleZipChange(e.target.value)} />
setProfileForm({...profileForm, address: e.target.value})} />
setProfileForm({...profileForm, city: e.target.value})} />
setProfileForm({...profileForm, state: e.target.value.toUpperCase().slice(0, 2)})} />
setProfileForm({...profileForm, phone: formatPhone(e.target.value)})} maxLength={16} />
setProfileForm({...profileForm, email: e.target.value})} />
{/* POSTGRESQL CARD */}

Banco de Dados

{systemStats ? ( Online ) : ( )}

Tamanho em Disco

{systemStats?.postgres?.dbSize || '--'}

Tabelas SGBD

{systemStats?.postgres?.tableCount || '--'} PostgreSQL

{/* MINIO STORAGE CARD */}

Storage Físico

{systemStats && !systemStats.minio?.error ? ( MinIO ) : ( Backup )}

Uso Total

{systemStats?.minio?.totalSizeMB || '0.00'} MB

Arquivos

{systemStats?.minio?.totalItems || '0'}

Dados do System

Evolution API

{data.evolutionConfig?.apiUrl ? (

URL: {data.evolutionConfig.apiUrl}

Instância: {data.evolutionConfig.instanceName}

API Key: ••••••••

) : (

Nenhuma credencial configurada.

)}

Responsável Legal / Diretor

{currentDirector ? (

Nome: {currentDirector.name}

CPF: {currentDirector.cpf}

Este responsável assinará automaticamente os documentos.

) : (

Nenhum diretor localizado. Cadastre um funcionário como Diretor na aba Funcionários.

)}
) : (

Logs de API

{apiLogs.map((log, i) => ( ))} {apiLogs.length === 0 && ( )}
Data Serviço Ação Detalhes
{new Date(log.date).toLocaleString()} {log.service} {log.action} {JSON.stringify(log.details)}
Nenhum log encontrado.
)} {/* Evolution API Modal */} {showEvolutionModal && (

Credenciais Evolution API

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" />
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" />
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="••••••••••••" />
)} {/* Storage Explorer Modal */} {showStorageManagerModal && (
{/* Header */}
{selectedStorageBucket ? ( ) : (
)}

{selectedStorageBucket ? selectedStorageBucket : 'Gerenciador de Arquivos'}

{selectedStorageBucket ? `${storageObjects.length} arquivos encontrados.` : `${systemStats?.minio?.buckets?.length || 0} pastas na nuvem.`}

{/* Content Body */}
{!selectedStorageBucket ? ( // Pastas (Buckets) View
{systemStats?.minio?.buckets?.map((b: any, idx: number) => (
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">

{b.name}

{b.items} arquivos • {b.sizeMB} MB

))} {(!systemStats?.minio?.buckets || systemStats.minio.buckets.length === 0) && (

Nenhuma pasta encontrada.

)}
) : loadingBucket ? (

Acessando MinIO Storage...

) : storageObjects.length === 0 ? (

Pasta Vazia

Nenhum arquivo encontrado neste bucket.

) : (
{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 (
{/* Thumbnail Area */}
{isImage ? ( {obj.key} ) : isPdf ? ( ) : ( )} {/* Hover Actions Overlay */}
{(isImage || isPdf) && ( )}
{/* File Info */}

{obj.key.split('/').pop()}

{sizeKB} KB
); })}
)}
)} {/* Database Explorer Modal */} {showDatabaseExplorerModal && (
{/* Header */}
{selectedDbTable ? ( ) : (
)}

{selectedDbTable ? selectedDbTable : 'Database Explorer'}

{selectedDbTable ? `${tableData.rows.length} registros exibidos.` : `${dbTables.length} tabelas no schema public do PostgreSQL.`}

{/* Content Body */}
{!selectedDbTable ? ( // Lista de Tabelas loadingDbTables ? (

Analisando Estrutura do PostgreSQL...

) : dbTables.length === 0 ? (

Nenhuma tabela encontrada

) : (
{dbTables.map((table, idx) => (
openTable(table.table_name)} className="group bg-white p-6 rounded-2xl border border-slate-200 shadow-sm hover:border-blue-300 hover:shadow-xl transition-all cursor-pointer flex items-center justify-between hover:-translate-y-1">

{table.table_name}

{table.row_count} registros
{table.total_size}
))}
) ) : loadingTableData ? ( // Carregando Dados da Tabela

Buscando registros...

) : tableData.rows.length === 0 ? ( // Tabela Vazia

Tabela Vazia

Nenhum registro encontrado nesta tabela.

) : ( // Visualização de Dados (Grid)
{tableData.fields.map((field, idx) => ( ))} {tableData.rows.map((row, rowIdx) => ( {tableData.fields.map((field, colIdx) => ( ))} ))}
{field}
{typeof row[field] === 'object' && row[field] !== null ? JSON.stringify(row[field]).substring(0, 50) + (JSON.stringify(row[field]).length > 50 ? '...' : '') : String(row[field] ?? '').substring(0, 50) + (String(row[field] ?? '').length > 50 ? '...' : '')}
)}
)} {/* Lightbox Preview */} {previewUrl && (
{/* Overlay transparente para fechar ao clicar fora */}
setPreviewUrl(null)}>
{/* Botão de fechar fixado na moldura */} {previewUrl.match(/\.(jpeg|jpg|gif|png|webp)$/i) ? ( Preview ) : (