import React, { useState, useEffect, createContext, useContext, useRef } from 'react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged, createUserWithEmailAndPassword, signInWithEmailAndPassword, signOut } from 'firebase/auth'; import { getFirestore, doc, getDoc, setDoc, collection, query, onSnapshot } from 'firebase/firestore'; import { LineChart, Line, BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; // Context para gerenciar o estado global da aplicação (usuário, tema, etc.) const AppContext = createContext(); // Hook personalizado para usar o contexto da aplicação const useAppContext = () => useContext(AppContext); // Função para converter base64 para ArrayBuffer (necessário para o áudio TTS, mas não usado diretamente aqui) function base64ToArrayBuffer(base64) { const binaryString = window.atob(base64); const len = binaryString.length; const bytes = new Uint8Array(len); for (let i = 0; i < len; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes.buffer; } // Função para converter PCM para WAV (necessário para o áudio TTS, mas não usado diretamente aqui) function pcmToWav(pcmData, sampleRate) { const numChannels = 1; // Mono audio const bytesPerSample = 2; // 16-bit PCM const wavHeader = new ArrayBuffer(44); const view = new DataView(wavHeader); // RIFF chunk descriptor writeString(view, 0, 'RIFF'); view.setUint32(4, 36 + pcmData.byteLength, true); // ChunkSize writeString(view, 8, 'WAVE'); // FMT sub-chunk writeString(view, 12, 'fmt '); view.setUint32(16, 16, true); // Subchunk1Size (16 for PCM) view.setUint16(20, 1, true); // AudioFormat (1 for PCM) view.setUint16(22, numChannels, true); view.setUint32(24, sampleRate, true); view.setUint32(28, sampleRate * numChannels * bytesPerSample, true); // ByteRate view.setUint16(32, numChannels * bytesPerSample, true); // BlockAlign view.setUint16(34, bytesPerSample * 8, true); // BitsPerSample // DATA sub-chunk writeString(view, 36, 'data'); view.setUint32(40, pcmData.byteLength, true); // Subchunk2Size const wavBlob = new Blob([wavHeader, pcmData], { type: 'audio/wav' }); return wavBlob; } function writeString(view, offset, string) { for (let i = 0; i < string.length; i++) { view.setUint8(offset + i, string.charCodeAt(i)); } } // Configuração do Firebase (variáveis globais fornecidas pelo ambiente Canvas) const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; const firebaseConfig = JSON.parse(typeof __firebase_config !== 'undefined' ? __firebase_config : '{}'); // Inicializa o Firebase const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); // Componente de Mensagem Flutuante (substitui alert/confirm) const MessageModal = ({ message, onClose }) => { if (!message) return null; return (

{message}

); }; // Componente de Autenticação const Auth = () => { const [isLogin, setIsLogin] = useState(true); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const { setUser, showMessageModal } = useAppContext(); const handleAuthAction = async (e) => { e.preventDefault(); try { if (isLogin) { await signInWithEmailAndPassword(auth, email, password); showMessageModal('Login realizado com sucesso!'); } else { await createUserWithEmailAndPassword(auth, email, password); showMessageModal('Cadastro realizado com sucesso!'); } } catch (error) { console.error("Erro de autenticação:", error); showMessageModal(`Erro: ${error.message}`); } }; useEffect(() => { const unsubscribe = onAuthStateChanged(auth, async (user) => { if (user) { setUser(user); } else { // Tenta fazer login anônimo se não houver usuário logado e não houver token inicial if (typeof __initial_auth_token === 'undefined' || __initial_auth_token === '') { try { await signInAnonymously(auth); } catch (error) { console.error("Erro ao fazer login anônimo:", error); } } } }); return () => unsubscribe(); }, [setUser]); useEffect(() => { // Tenta fazer login com o token inicial se disponível const signInWithToken = async () => { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token !== '') { try { await signInWithCustomToken(auth, __initial_auth_token); } catch (error) { console.error("Erro ao fazer login com token customizado:", error); } } }; signInWithToken(); }, []); return (

{isLogin ? 'Entrar' : 'Cadastrar'}

setEmail(e.target.value)} required />
setPassword(e.target.value)} required />

{isLogin ? 'Não tem uma conta?' : 'Já tem uma conta?'}

); }; // Componente de Dashboard const Dashboard = () => { const { user, showMessageModal, accentColor, isFullScreen } = useAppContext(); // Removido spreadsheetData e setSpreadsheetData const [loading, setLoading] = useState(true); const [chartLayouts, setChartLayouts] = useState([]); // Estados para arrastar e redimensionar const [activeChartId, setActiveChartId] = useState(null); const [isDragging, setIsDragging] = useState(false); const [isResizing, setIsResizing] = useState(false); const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); const [resizeInitial, setResizeInitial] = useState({ mouseX: 0, mouseY: 0, width: 0, height: 0 }); // Dados de exemplo para o gráfico (agora são os dados que os gráficos usarão diretamente) const defaultChartData = [ { name: 'Jan', pv: 4000, uv: 2400 }, { name: 'Fev', pv: 3000, uv: 1398 }, { name: 'Mar', pv: 2000, uv: 9800 }, { name: 'Abr', pv: 2780, uv: 3908 }, { name: 'Mai', pv: 1890, uv: 4800 }, { name: 'Jun', pv: 2390, uv: 3800 }, { name: 'Jul', pv: 3490, uv: 4300 }, ]; // Cores para o Pie Chart const PIE_COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#AF19FF', '#FF19A3']; useEffect(() => { if (user && user.uid) { const userDocRef = doc(db, `artifacts/${appId}/users/${user.uid}/dashboards`, 'userDashboard'); const unsubscribe = onSnapshot(userDocRef, (docSnap) => { if (docSnap.exists()) { const data = docSnap.data(); // Não carrega mais spreadsheetData if (data.layouts && data.layouts.length > 0) { setChartLayouts(data.layouts); } else { // Layout padrão se nenhum layout for salvo setChartLayouts([ { id: 'chart1', x: 10, y: 10, width: 600, height: 350, type: 'line' }, { id: 'chart2', x: 650, y: 10, width: 400, height: 300, type: 'bar' } ]); } } else { // Não define spreadsheetData padrão // Layout padrão se o documento não existir setChartLayouts([ { id: 'chart1', x: 10, y: 10, width: 600, height: 350, type: 'line' }, { id: 'chart2', x: 650, y: 10, width: 400, height: 300, type: 'bar' } ]); } setLoading(false); }, (error) => { console.error("Erro ao buscar dados do dashboard:", error); showMessageModal(`Erro ao carregar dados: ${error.message}`); setLoading(false); }); return () => unsubscribe(); } }, [user, showMessageModal]); const saveDashboardConfig = async (currentLayouts) => { // Removido currentSpreadsheetData if (user && user.uid) { try { const userDocRef = doc(db, `artifacts/${appId}/users/${user.uid}/dashboards`, 'userDashboard'); await setDoc(userDocRef, { // Não salva mais spreadsheetData layouts: currentLayouts, // Salva os layouts lastUpdated: new Date() }, { merge: true }); showMessageModal('Configurações do dashboard salvas com sucesso!'); } catch (error) { console.error("Erro ao salvar dados do dashboard:", error); showMessageModal(`Erro ao salvar dados: ${error.message}`); } } else { showMessageModal('Você precisa estar logado para salvar as configurações.'); } }; // Atualiza a posição e o tamanho de um gráfico específico const updateChartLayout = (id, newX, newY, newWidth, newHeight) => { setChartLayouts(prevLayouts => { const updatedLayouts = prevLayouts.map(chart => chart.id === id ? { ...chart, x: newX, y: newY, width: newWidth, height: newHeight } : chart ); // Salva automaticamente o layout após arrastar/redimensionar saveDashboardConfig(updatedLayouts); return updatedLayouts; }); }; // Adiciona um novo cartão de gráfico ao dashboard const addChart = (type = 'empty') => { const newId = `chart-${Date.now()}`; const newLayout = { id: newId, x: 50, y: 50, width: 500, height: 300, type: type }; setChartLayouts(prevLayouts => { const updatedLayouts = [...prevLayouts, newLayout]; saveDashboardConfig(updatedLayouts); // Salva o novo layout return updatedLayouts; }); }; // Exclui um gráfico do dashboard const handleDeleteChart = (chartId) => { const updatedLayouts = chartLayouts.filter(chart => chart.id !== chartId); setChartLayouts(updatedLayouts); saveDashboardConfig(updatedLayouts); // Salva o layout atualizado após a exclusão showMessageModal('Gráfico excluído com sucesso!'); }; // Inicia o arrastar do gráfico const onDragStart = (e, chart) => { e.stopPropagation(); // Evita a seleção de texto setIsDragging(true); setActiveChartId(chart.id); setDragOffset({ x: e.clientX - chart.x, y: e.clientY - chart.y, }); }; // Inicia o redimensionamento do gráfico const onResizeStart = (e, chart) => { e.stopPropagation(); // Evita que o arrastar comece setIsResizing(true); setActiveChartId(chart.id); setResizeInitial({ mouseX: e.clientX, mouseY: e.clientY, width: chart.width, height: chart.height, }); }; useEffect(() => { const handleMouseMove = (e) => { if (isDragging && activeChartId) { setChartLayouts(prevLayouts => prevLayouts.map(chart => { if (chart.id === activeChartId) { const newX = e.clientX - dragOffset.x; const newY = e.clientY - dragOffset.y; return { ...chart, x: newX, y: newY }; } return chart; }) ); } else if (isResizing && activeChartId) { setChartLayouts(prevLayouts => prevLayouts.map(chart => { if (chart.id === activeChartId) { // Garante um tamanho mínimo para o gráfico const newWidth = Math.max(200, resizeInitial.width + (e.clientX - resizeInitial.mouseX)); const newHeight = Math.max(150, resizeInitial.height + (e.clientY - resizeInitial.mouseY)); return { ...chart, width: newWidth, height: newHeight }; } return chart; }) ); } }; const handleMouseUp = () => { if (isDragging || isResizing) { // As posições/tamanhos finais já foram atualizados em handleMouseMove // Aqui apenas resetamos os flags e salvamos o layout final const chart = chartLayouts.find(c => c.id === activeChartId); if (chart) { updateChartLayout(chart.id, chart.x, chart.y, chart.width, chart.height); } setIsDragging(false); setIsResizing(false); setActiveChartId(null); } }; // Adiciona listeners globais para mousemove e mouseup window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); // Limpeza dos listeners quando o componente é desmontado return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); }; }, [isDragging, isResizing, activeChartId, dragOffset, resizeInitial, chartLayouts]); if (loading) { return (
Carregando dashboards...
); } return (
{!isFullScreen && (

Seu Dashboard

)} {!isFullScreen && (

Gerenciar Gráficos

)} {/* Container para os gráficos arrastáveis e redimensionáveis */}
{chartLayouts.map((chart) => (
onDragStart(e, chart)} // Handle de arrastar > {chart.type === 'line' && (

Gráfico de Linha

)} {chart.type === 'bar' && (

Gráfico de Barra

)} {chart.type === 'pie' && (

Gráfico de Pizza

{ defaultChartData.map((entry, index) => ( )) }
)} {chart.type === 'empty' && (

Cartão Vazio (placeholder para futuro gráfico)

)} {/* Botão de Excluir */} {!isFullScreen && ( )} {/* Handle de redimensionamento (canto inferior direito) */} {!isFullScreen && (
onResizeStart(e, chart)} style={{ zIndex: 21 }} // Garante que o handle de redimensionamento esteja no topo >
)}
))}
); }; // Componente de Perfil do Usuário const Profile = () => { const { user, theme, setTheme, accentColor, setAccentColor, showMessageModal } = useAppContext(); const [profilePhoto, setProfilePhoto] = useState(''); const [companyLogo, setCompanyLogo] = useState(''); const [userName, setUserName] = useState(user?.displayName || ''); const [userEmail, setUserEmail] = useState(user?.email || ''); const userId = user?.uid || 'N/A'; useEffect(() => { if (user && user.uid) { const userProfileRef = doc(db, `artifacts/${appId}/users/${user.uid}/profile`, 'userProfile'); const unsubscribe = onSnapshot(userProfileRef, (docSnap) => { if (docSnap.exists()) { const data = docSnap.data(); setProfilePhoto(data.profilePhoto || ''); setCompanyLogo(data.companyLogo || ''); setUserName(data.userName || user?.displayName || ''); // Não atualiza o email aqui para evitar loop, o email vem do Firebase Auth } }, (error) => { console.error("Erro ao buscar perfil do usuário:", error); showMessageModal(`Erro ao carregar perfil: ${error.message}`); }); return () => unsubscribe(); } }, [user, showMessageModal]); const handleSaveProfile = async () => { if (user && user.uid) { try { const userProfileRef = doc(db, `artifacts/${appId}/users/${user.uid}/profile`, 'userProfile'); await setDoc(userProfileRef, { profilePhoto, companyLogo, userName, accentColor, // Salva a cor de destaque no perfil }, { merge: true }); showMessageModal('Perfil salvo com sucesso!'); } catch (error) { console.error("Erro ao salvar perfil:", error); showMessageModal(`Erro ao salvar perfil: ${error.message}`); } } else { showMessageModal('Você precisa estar logado para salvar o perfil.'); } }; const handlePhotoUpload = (e, type) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onloadend = () => { if (type === 'profile') { setProfilePhoto(reader.result); } else if (type === 'logo') { setCompanyLogo(reader.result); } }; reader.readAsDataURL(file); showMessageModal(`Funcionalidade de upload de ${type === 'profile' ? 'foto' : 'logo'} em desenvolvimento. Imagem selecionada.`); } }; return (

Seu Perfil

Informações do Usuário

setUserName(e.target.value)} className="shadow appearance-none border rounded-lg w-full py-2 px-3 text-gray-700 dark:text-gray-200 leading-tight focus:outline-none focus:shadow-outline bg-gray-50 dark:bg-gray-700 border-gray-300 dark:border-gray-600" />

{userId}

Personalização Visual

handlePhotoUpload(e, 'profile')} className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-100 file:text-blue-700 hover:file:bg-blue-200 transition duration-300 ease-in-out" /> {profilePhoto && ( Foto de Perfil )}
handlePhotoUpload(e, 'logo')} className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-green-100 file:text-green-700 hover:file:bg-green-200 transition duration-300 ease-in-out" /> {companyLogo && ( Logo da Empresa )}
setAccentColor(e.target.value)} className="w-16 h-10 rounded-lg border-none cursor-pointer" style={{ backgroundColor: accentColor }} /> {accentColor}
); }; // Componente Principal da Aplicação const App = () => { const [user, setUser] = useState(null); const [currentPage, setCurrentPage] = useState('auth'); const [theme, setTheme] = useState(() => localStorage.getItem('theme') || 'light'); const [accentColor, setAccentColor] = useState(() => localStorage.getItem('accentColor') || '#3b82f6'); // Tailwind blue-500 const [messageModal, setMessageModal] = useState(''); const [isFullScreen, setIsFullScreen] = useState(false); // Novo estado para tela cheia const showMessageModal = (message) => { setMessageModal(message); }; const closeMessageModal = () => { setMessageModal(''); }; useEffect(() => { // Listener para o estado de autenticação do Firebase const unsubscribeAuth = onAuthStateChanged(auth, async (currentUser) => { if (currentUser) { setUser(currentUser); setCurrentPage('dashboard'); // Redireciona para o dashboard após login } else { setUser(null); setCurrentPage('auth'); // Volta para a tela de autenticação } }); // Carrega o tema e a cor de destaque do localStorage const savedTheme = localStorage.getItem('theme'); if (savedTheme) { setTheme(savedTheme); } const savedAccentColor = localStorage.getItem('accentColor'); if (savedAccentColor) { setAccentColor(savedAccentColor); } return () => unsubscribeAuth(); }, []); useEffect(() => { // Aplica o tema ao body e salva no localStorage if (theme === 'dark') { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } localStorage.setItem('theme', theme); }, [theme]); useEffect(() => { // Aplica a cor de destaque como variável CSS e salva no localStorage document.documentElement.style.setProperty('--accent-color', accentColor); document.documentElement.style.setProperty('--accent-500', accentColor); // Para cores secundárias, você pode derivar ou usar cores fixas do Tailwind document.documentElement.style.setProperty('--accent-600', accentColor); // Exemplo, idealmente calcular um tom mais escuro localStorage.setItem('accentColor', accentColor); }, [accentColor]); // Listener para a tecla ESC sair do modo tela cheia useEffect(() => { const handleKeyDown = (e) => { if (e.key === 'Escape' && isFullScreen) { setIsFullScreen(false); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [isFullScreen]); const handleSignOut = async () => { try { await signOut(auth); showMessageModal('Desconectado com sucesso!'); } catch (error) { console.error("Erro ao desconectar:", error); showMessageModal(`Erro ao desconectar: ${error.message}`); } }; // Define as variáveis CSS para o tema const themeClasses = theme === 'dark' ? 'dark' : ''; const bgColor = theme === 'dark' ? '#1a202c' : '#f7fafc'; // bg-gray-900 ou bg-gray-100 const textColor = theme === 'dark' ? '#e2e8f0' : '#2d3748'; // text-gray-200 ou text-gray-800 return (
{user ? (
{/* Barra de Navegação - Escondida em modo tela cheia */} {!isFullScreen && ( )} {/* Botão para sair do modo tela cheia (visível apenas em tela cheia) */} {isFullScreen && ( )}
{/* Garante que o main ocupe a altura total em tela cheia */} {currentPage === 'dashboard' && } {currentPage === 'profile' && }
) : ( )}
); }; export default App;