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) => 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([]); 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 [isSyncing, setIsSyncing] = useState(false); const supabaseConfigured = useMemo(() => isSupabaseConfigured(), []); const [showImportModal, setShowImportModal] = useState(false); const [isClosing, setIsClosing] = useState(false); const downloadSupabaseSQL = () => { const sql = `-- Create the table for storing the entire application state as a JSON blob create table if not exists school_data ( id bigint primary key, data jsonb not null default '{}'::jsonb, updated_at timestamp with time zone default timezone('utc'::text, now()) not null ); -- Insert the initial row (id=1) if it doesn't exist so the app has something to fetch/update insert into school_data (id, data) values (1, '{}'::jsonb) on conflict (id) do nothing; -- Enable Row Level Security (RLS) alter table school_data enable row level security; -- Create a policy that allows anyone to read/write (for development/demo purposes) -- In a real production app, you would restrict this to authenticated users create policy "Enable read access for all users" on school_data for select using (true); create policy "Enable insert access for all users" on school_data for insert with check (true); create policy "Enable update access for all users" on school_data for update using (true);`; const blob = new Blob([sql], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'supabase_setup.sql'; a.click(); URL.revokeObjectURL(url); }; const closeModal = () => { setIsClosing(true); setTimeout(() => { setShowImportModal(false); setIsClosing(false); }, 300); }; 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); let logoUrl = ''; // Try to upload to Supabase if configured if (supabaseConfigured) { const url = await uploadLogo(compressedFile); if (url) { logoUrl = url; } } // Fallback to base64 if Supabase upload failed or not configured if (!logoUrl) { const reader = new FileReader(); logoUrl = await new Promise((resolve) => { reader.onload = (e) => resolve(e.target?.result as string); reader.readAsDataURL(compressedFile); }); } setGlobalLogo(logoUrl); updateData({ logo: logoUrl }); 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 handleManualSync = async () => { if (!supabaseConfigured) return; setIsSyncing(true); try { const cloudData = await dbService.fetchFromCloud(); if (cloudData) { setData(cloudData); await dbService.saveData(cloudData); showAlert('Sucesso', '✅ Dados sincronizados com a nuvem!', 'success'); } else { // If no cloud data, maybe we should push local data? await dbService.saveToCloud(data); showAlert('Sucesso', '✅ Dados locais enviados para a nuvem!', 'success'); } } catch (error) { showAlert('Erro', '❌ Falha na sincronização. Verifique sua conexão e configurações.', 'error'); } finally { setIsSyncing(false); } }; 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})} />

Sincronização Nuvem

{supabaseConfigured ? : } {supabaseConfigured ? 'Conectado ao Supabase' : 'Não Conectado'}

{supabaseConfigured ? 'Seus dados estão sendo salvos automaticamente na nuvem.' : 'Para habilitar o backup na nuvem, configure as variáveis de ambiente VITE_SUPABASE_URL e VITE_SUPABASE_KEY.'}

{!supabaseConfigured && (
VITE_SUPABASE_URL=...
VITE_SUPABASE_KEY=...
)}
{supabaseConfigured && (

Sincronização Automática Ativa

)}

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="••••••••••••" />
)}
); }; export default Settings;