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 (
);
};
// 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'}
{isLogin ? 'Não tem uma conta?' : 'Já tem uma conta?'}
setIsLogin(!isLogin)}
className="text-accent-500 hover:text-accent-600 font-bold ml-1 focus:outline-none"
>
{isLogin ? 'Cadastre-se' : 'Faça login'}
);
};
// 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
saveDashboardConfig(chartLayouts)}
className="mt-4 mr-2 bg-accent-500 hover:bg-accent-600 text-white font-bold py-2 px-4 rounded-full transition duration-300 ease-in-out transform hover:scale-105"
>
Salvar Layout
addChart('line')}
className="mt-4 bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-full transition duration-300 ease-in-out transform hover:scale-105"
>
Adicionar Gráfico de Linha
addChart('bar')}
className="mt-4 ml-2 bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded-full transition duration-300 ease-in-out transform hover:scale-105"
>
Adicionar Gráfico de Barra
addChart('pie')}
className="mt-4 ml-2 bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded-full transition duration-300 ease-in-out transform hover:scale-105"
>
Adicionar Gráfico de Pizza
addChart('empty')}
className="mt-4 ml-2 bg-purple-500 hover:bg-purple-600 text-white font-bold py-2 px-4 rounded-full transition duration-300 ease-in-out transform hover:scale-105"
>
Adicionar Cartão Vazio
)}
{/* 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 && (
{ e.stopPropagation(); handleDeleteChart(chart.id); }}
className="absolute top-2 right-2 bg-red-500 hover:bg-red-600 text-white w-6 h-6 flex items-center justify-center rounded-full text-sm font-bold transition duration-300 ease-in-out transform hover:scale-110 z-30"
title="Excluir Gráfico"
>
X
)}
{/* 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 (
);
};
// 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 && (
Meu Dashboard
setCurrentPage('dashboard')}
className={`px-4 py-2 rounded-full font-semibold transition duration-300 ease-in-out ${
currentPage === 'dashboard' ? 'bg-accent-500 text-white' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
>
Dashboard
setCurrentPage('profile')}
className={`px-4 py-2 rounded-full font-semibold transition duration-300 ease-in-out ${
currentPage === 'profile' ? 'bg-accent-500 text-white' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
>
Perfil
setIsFullScreen(true)} // Botão para entrar em tela cheia
className="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded-full transition duration-300 ease-in-out transform hover:scale-105"
>
Modo Apresentação
Sair
)}
{/* Botão para sair do modo tela cheia (visível apenas em tela cheia) */}
{isFullScreen && (
setIsFullScreen(false)}
className="fixed top-4 right-4 bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-full z-50 transition duration-300 ease-in-out transform hover:scale-105"
>
Sair do Modo Apresentação
)}
{/* Garante que o main ocupe a altura total em tela cheia */}
{currentPage === 'dashboard' && }
{currentPage === 'profile' && }
) : (
)}
);
};
export default App;