diff --git a/README.md b/README.md index da444cc..0bfc5ef 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,47 @@ -# CondoMaster Pro +# CondoMasterResults -![CondoMaster Pro Preview](https://via.placeholder.com/800x400.png?text=CondoMaster+Pro+-+Gestão+de+Condomínios) +Uma plataforma moderna de gestão de condomínios focada em transparência, comunicação em tempo real e facilidade de uso, tanto para moradores como para a administração. -O **CondoMaster Pro** é uma aplicação web moderna e responsiva (Single Page Application - SPA) desenvolvida para simplificar e digitalizar a gestão de condomínios. Desenhado a pensar tanto na entidade gestora (Administradores) como nos habitantes (Moradores), o sistema integra todas as comunicações, finanças e ocorrências do dia a dia. +## 🎯 Sobre o Projeto + +O **CondoMasterResults** é uma *Single Page Application* concebida para digitalizar a gestão do dia a dia num condomínio. Permite aos moradores consultar despesas, reservar espaços comuns e reportar ocorrências, enquanto oferece aos administradores um painel de controlo completo sobre as finanças e os utilizadores. + +## 🚀 Funcionalidades Chave + +* **Perfis Diferenciados**: Acessos específicos para Administradores (gestão total) e Moradores (painel simplificado). +* **Fórum em Tempo Real**: Chat integrado (Mural) sincronizado instantaneamente entre todos os vizinhos. +* **Gestão de Espaços Comuns**: Sistema prático de reservas para Ginásio, Salão de Festas e Parque de Jogos. +* **Portal de Manutenção**: Plataforma para reportar avarias (moradores) e gerir a resolução das mesmas (administração). +* **Gestão Financeira e Faturação**: Visão clara sobre quotas pendentes, geração de faturas e fluxo de caixa. + +## 🛠️ Tecnologias Utilizadas + +Esta aplicação corre integralmente no lado do cliente com forte integração de serviços cloud: + +- **Frontend**: React (UI Declarativa), Tailwind CSS (Estilização) e Lucide React (Ícones). +- **Backend & Base de Dados**: Firebase Auth (Autenticação) e Firebase Realtime Database (Sincronização de dados em direto). + +## 📥 Como Começar + +Uma vez que a aplicação está desenhada para correr diretamente no navegador (recorrendo ao Firebase para armazenar estado): + +1. Clona ou descarrega este repositório para o teu computador. +2. Abre a pasta do projeto num servidor local. Recomendamos o uso de ferramentas como o **Live Server** (extensão VS Code) ou a execução de `npx serve` no terminal. +3. Acede ao endereço gerado no teu navegador (ex: `http://localhost:3000`). + +### 🔑 Credenciais de Teste + +Podes explorar a plataforma utilizando as seguintes contas de demonstração: + +**Administrador** +- Email: `administradores@gmail.com` +- Senha: `admin123` + +**Morador** +- Email: `moradores@gmail.com` +- Senha: `moradores123` + +*(Nota: É possível testar o envio de mensagens em tempo real abrindo duas janelas com contas diferentes lado a lado).* --- - -## ✨ Funcionalidades Principais - -### Acesso Baseado em Perfis (Role-Based Access Control) -* **🧑‍💻 Administradores (`admin`)**: Visão 360º. Podem gerir moradores, registar receitas e despesas globais, cobrar dívidas, enviar faturas manuais (com um clique) e alterar papéis de acesso ("promover" ou "despromover"). -* **🏠 Moradores (`morador`)**: Painel simplificado desenhado para transparência. Permite verificar as próprias quotas em atraso, reportar danos/anomalias (manutenção) e reservar espaços comuns. - -### 💰 Faturação e Gestão Financeira (Exclusivo Admins) -- Visão geral completa de Fluxo de Caixa (Despesas vs. Receitas). -- Emissão instantânea de recibos avulso. -- Notificações de dívidas encaminhadas com apenas um clique na tabela integrada de **Faturação**. - -### 📅 Gestão de Reservas -* **Lista e Mapa**: Sistema visual de reservas em três ginásios, salões de festas e parques de jogos. -* **Histórico Completo**: Página exclusiva para listagem de todas as reservas agendadas, acessível a todas as entidades. - -### 🛠️ Ocorrências e Manutenção -- Secção para os condóminos relatarem problemas no edifício (ex: candeeiros partidos, problemas de elevador) indicando o grau de severidade. -- Os administradores avaliam a prioridade, resolvem as ocorrências digitalmente e mantêm os residentes notificados do estado. - -### 🎨 Design Moderno & UI Inteligente -* Compatível com **Mobile e Desktop**. -* Inclui um switch suave para **Modo Escuro (Dark Mode)**, Modo Claro e deteção por Sistema, integrados perfeitamente no menu de perfil. -* Sistema de notificações do tipo Themed/Toasts para validações imediatas (Confirmações, Erros, Avisos). - ---- - -## 💻 Stack Tecnológica - -O projeto foi construído usando uma arquitetura modular moderna num formato de ficheiro de entrada principal que integra os ecossistemas: - -* **React**: Implementado diretamente do navegador (sem build step local). Geração de componentes declarativos (UI Dinâmica). -* **Tailwind CSS**: Carregado dinamicamente para aplicar estilos sofisticados e reativos, acelerando o desenvolvimento visual da interface. -* **Lucide React**: Biblioteca adotada inteiramente para a vasta panóplia de ícones (`lucide-react`). - ---- - -## 🚀 Como Iniciar (Quick Start) - -Visto que o projeto já traz toda a lógica baseada na Web injetada, não é precisa uma instalação exaustiva na máquina. - -1. **Baixar o Projeto:** - Basta que tenhas o ficheiro principal (geralmente `index.html`) e o ambiente disponível na mesma pasta (neste caso `GestorCondominio`). - -2. **Abrir a Aplicação:** - - Para pré-visualizar rapidamente a aplicação, podes apenas fazer duplo-clique no **`index.html`** para abrir o sistema num browser moderno. - - Alternativamente, podes hospedar este ficheiro num serviço de Live Server ou num host online (ex: Vercel, Netlify, Github Pages), não existindo configuração complexa. - ---- - -## 🔐 Credenciais de Base (Ambiente de Testes) - -Neste momento as credenciais estão pré-programadas para experimentação do comportamento do sistema: - -**Acesso de Administrador:** -- **Email:** `administradores@gmail.com` -- **Palavra-passe:** `admin123` - -**Acesso de Morador:** -- **Email:** `moradores@gmail.com` -- **Palavra-passe:** `moradores123` - -*(Nota: Alguns moradores registados na base de dados fictícia no "Estado" da app podem aceder através da palavra-passe padrão de morador ou usando o respetivo contacto telefónico)*. - ---- - -## 👨‍🔧 Desenvolvimento e Melhorias Mapeadas -* Ligação completa de base de dados escalável com a inicialização nativa contida do **Firebase**. -* Emissão e importação de documentos faturação automatizados PDF. - -***Desenvolvido para criar comunidades perfeitamente ligadas.*** +*Construído com simplicidade e foco na comunidade.* diff --git a/index.html b/index.html index 25b0e88..532b751 100644 --- a/index.html +++ b/index.html @@ -99,7 +99,7 @@ MessageCircle, Paperclip, Send } from 'lucide-react'; import { app } from './firebase.js'; - import { getAuth, signInWithEmailAndPassword } from 'https://www.gstatic.com/firebasejs/12.1.0/firebase-auth.js'; + import { getAuth, signInWithEmailAndPassword, createUserWithEmailAndPassword } from 'https://www.gstatic.com/firebasejs/12.1.0/firebase-auth.js'; import { getDatabase, ref, push, set, onValue, remove } from 'https://www.gstatic.com/firebasejs/12.1.0/firebase-database.js'; const auth = getAuth(app); @@ -257,7 +257,119 @@ ); }; - const LoginView = ({ onLogin }) => { + const WaitingApprovalView = ({ onLogout }) => { + return ( +
+
+
+ +
+

Conta Pendente

+

+ O seu registo foi concluído com sucesso, mas a sua conta aguarda aprovação da administração. Por favor, aguarde. +

+ +
+
+ ); + }; + + const RegisterView = ({ onToggleView, onRegister }) => { + const [formData, setFormData] = useState({ + name: '', dob: '', phone: '', cc: '', nif: '', unit: '', email: '', password: '', confirmPassword: '' + }); + const [error, setError] = useState(''); + + const isFormValid = Object.values(formData).every(val => val.trim() !== '') && formData.password === formData.confirmPassword; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (formData.password !== formData.confirmPassword) { + setError('As palavras-passe não coincidem.'); + return; + } + const result = await onRegister(formData); + if (!result.success) { + setError(result.message || 'Ocorreu um erro ao tentar registar a conta.'); + } + }; + + const handleChange = (e) => { + setFormData({...formData, [e.target.name]: e.target.value}); + }; + + return ( +
+
+
+

Criar Conta

+

Registo de Novo Morador

+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + {error && ( +
+ + {error} +
+ )} + +
+ +
+ +
+
+
+
+
+ ); + }; + + const LoginView = ({ onLogin, onToggleView }) => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); @@ -311,9 +423,16 @@ {error} )} - +
+ +
+ +
+
@@ -332,43 +451,171 @@ const [userRole, setUserRole] = useState(() => { return sessionStorage.getItem('condo_role') || 'morador'; }); + const [currentUserName, setCurrentUserName] = useState(() => { + return sessionStorage.getItem('condo_user_name') || 'Utilizador'; + }); + const [currentUserId, setCurrentUserId] = useState(() => { + return sessionStorage.getItem('condo_user_id') || '0'; + }); + const [userStatus, setUserStatus] = useState(() => { + return sessionStorage.getItem('condo_user_status') || 'aprovado'; + }); + const [authView, setAuthView] = useState('login'); + + const handleRegister = async (data) => { + // Verificação na lista atual de moradores para prevenir duplicados locais + const emailExists = residents.some(r => r.email && r.email.toLowerCase() === data.email.toLowerCase()); + if (emailExists) { + return { success: false, message: 'Este email já se encontra registado no sistema.' }; + } + + try { + const userCredential = await createUserWithEmailAndPassword(auth, data.email, data.password); + const userId = userCredential.user.uid; + + await set(ref(db, `condominos/${userId}`), { + id: userId, + name: data.name, + email: data.email, + contact: data.phone, + dob: data.dob, + cc: data.cc, + nif: data.nif, + role: 'morador', + status: 'pendente', + unit: data.unit, + pending: 0 + }); + + sessionStorage.setItem('condo_auth', 'true'); + sessionStorage.setItem('condo_role', 'morador'); + sessionStorage.setItem('condo_user_name', data.name); + sessionStorage.setItem('condo_user_id', userId); + sessionStorage.setItem('condo_user_status', 'pendente'); + + setIsAuthenticated(true); + setUserRole('morador'); + setCurrentUserName(data.name); + setCurrentUserId(userId); + setUserStatus('pendente'); + return { success: true }; + } catch (error) { + console.error("Erro no registo Firebase:", error); + + if (error.code === 'auth/email-already-in-use') { + return { success: false, message: 'Este email já está associado a outra conta.' }; + } + if (error.code === 'auth/weak-password') { + return { success: false, message: 'A palavra-passe deve ter pelo menos 6 caracteres.' }; + } + if (error.code === 'auth/invalid-email') { + return { success: false, message: 'O formato do email é inválido.' }; + } + + // Se falhar devido a falta de configuração, simula registo local (fallback) + console.log("A executar fallback local de registo..."); + const localId = 'local_' + Date.now(); + + try { + await set(ref(db, `condominos/${localId}`), { + id: localId, + name: data.name, + email: data.email, + contact: data.phone, + dob: data.dob, + cc: data.cc, + nif: data.nif, + role: 'morador', + status: 'pendente', + unit: data.unit, + pending: 0, + contact: data.password // Guardado no campo contact apenas para o fallback mock local de login funcionar + }); + } catch(dbErr) { + console.error("Base de dados inacessível no fallback.", dbErr); + } + + sessionStorage.setItem('condo_auth', 'true'); + sessionStorage.setItem('condo_role', 'morador'); + sessionStorage.setItem('condo_user_name', data.name); + sessionStorage.setItem('condo_user_id', localId); + sessionStorage.setItem('condo_user_status', 'pendente'); + + setIsAuthenticated(true); + setUserRole('morador'); + setCurrentUserName(data.name); + setCurrentUserId(localId); + setUserStatus('pendente'); + return { success: true }; + } + }; const handleLogin = async (email, password) => { try { const userCredential = await signInWithEmailAndPassword(auth, email, password); let role = 'morador'; + let userName = 'Utilizador'; + let userId = userCredential.user.uid; + let status = 'aprovado'; + if (email.toLowerCase().includes('admin')) { role = 'admin'; + userName = 'Administração'; } else { - const residentUser = residents.find(r => r.email && r.email.toLowerCase() === email.toLowerCase()); + const residentUser = residents.find(r => r.id === userId || (r.email && r.email.toLowerCase() === email.toLowerCase())); if (residentUser) { role = residentUser.role || 'morador'; + userName = residentUser.name + (residentUser.unit && residentUser.unit !== 'Pendente' ? ` (${residentUser.unit})` : ''); + userId = residentUser.id || userId; + status = residentUser.status || 'aprovado'; + } else { + status = 'pendente'; // Fallback if missing } } sessionStorage.setItem('condo_auth', 'true'); sessionStorage.setItem('condo_role', role); + sessionStorage.setItem('condo_user_name', userName); + sessionStorage.setItem('condo_user_id', userId); + sessionStorage.setItem('condo_user_status', status); setIsAuthenticated(true); setUserRole(role); + setCurrentUserName(userName); + setCurrentUserId(userId); + setUserStatus(status); return true; } catch (error) { console.log("Firebase Auth falhou, a tentar conta local...", error); let role = null; + let userName = 'Utilizador'; + let userId = 'local_' + Date.now(); + + let status = 'aprovado'; + if (email === 'administradores@gmail.com' && password === 'admin123') { role = 'admin'; - } else if (email === 'moradores@gmail.com' && password === 'moradores123') { - role = 'morador'; + userName = 'Administração'; + userId = 'admin_001'; } else { const residentUser = residents.find(r => r.email && r.email.toLowerCase() === email.toLowerCase()); if (residentUser && (password === residentUser.contact || password === '1234')) { role = residentUser.role || 'morador'; + userName = residentUser.name + (residentUser.unit && residentUser.unit !== 'Pendente' ? ` (${residentUser.unit})` : ''); + userId = residentUser.id || userId; + status = residentUser.status || 'aprovado'; } } if (role) { sessionStorage.setItem('condo_auth', 'true'); sessionStorage.setItem('condo_role', role); + sessionStorage.setItem('condo_user_name', userName); + sessionStorage.setItem('condo_user_id', userId); + sessionStorage.setItem('condo_user_status', status); setIsAuthenticated(true); setUserRole(role); + setCurrentUserName(userName); + setCurrentUserId(userId); + setUserStatus(status); return true; } return false; @@ -379,8 +626,14 @@ if (window.confirm('Tem a certeza que deseja terminar sessão?')) { sessionStorage.removeItem('condo_auth'); sessionStorage.removeItem('condo_role'); + sessionStorage.removeItem('condo_user_name'); + sessionStorage.removeItem('condo_user_id'); + sessionStorage.removeItem('condo_user_status'); setIsAuthenticated(false); setUserRole(null); + setCurrentUserName('Utilizador'); + setCurrentUserId('0'); + setUserStatus('aprovado'); setActiveTab('dashboard'); } }; @@ -390,6 +643,9 @@ const [issues, setIssues] = useState([]); const [bookings, setBookings] = useState([]); const [invoices, setInvoices] = useState([]); + const [messages, setMessages] = useState([]); + const [newMessageText, setNewMessageText] = useState(''); + const [activeChat, setActiveChat] = useState({ type: 'global', id: 'global', name: 'Fórum do Condomínio' }); useEffect(() => { const loadData = (path, setter, sortFunc = null) => { @@ -415,6 +671,26 @@ unsubResidents(); unsubFinances(); unsubIssues(); unsubBookings(); unsubInvoices(); }; }, []); + + useEffect(() => { + let path = 'mural_mensagens'; + if (activeChat.type === 'private') { + path = `mensagens_privadas/${[currentUserId, activeChat.id].sort().join('_')}`; + } + + const unsub = onValue(ref(db, path), (snapshot) => { + const data = snapshot.val(); + if (data) { + let parsed = Object.entries(data).map(([id, val]) => ({ id, ...val })); + parsed = parsed.sort((a,b) => a.timestamp - b.timestamp); + setMessages(parsed); + } else { + setMessages([]); + } + }, (error) => console.error(`Erro ao carregar mensagens de ${path}:`, error)); + + return () => unsub(); + }, [activeChat, currentUserId]); const [notificationsList, setNotificationsList] = useState(INITIAL_NOTIFICATIONS); const [isNotificationsOpen, setNotificationsOpen] = useState(false); @@ -881,12 +1157,20 @@

Manutenção e Ocorrências

Gestão de pedidos e reparações

- + {userRole === 'admin' ? ( + + ) : ( + + )}
@@ -1117,7 +1401,13 @@ }; if (!isAuthenticated) { - return ; + return authView === 'login' + ? setAuthView('register')} /> + : setAuthView('login')} />; + } + + if (userStatus === 'pendente') { + return ; } return ( @@ -1149,6 +1439,7 @@ {userRole === 'admin' && { setActiveTab('residents'); setSidebarOpen(false); }} />} {userRole === 'admin' && { setActiveTab('finance'); setSidebarOpen(false); }} />} {userRole === 'admin' && { setActiveTab('billing'); setSidebarOpen(false); }} />} + {userRole === 'admin' && { setActiveTab('approvals'); setSidebarOpen(false); }} />} { setActiveTab('maintenance'); setSidebarOpen(false); }} /> { setActiveTab('messages'); setSidebarOpen(false); }} /> { setActiveTab('map'); setSidebarOpen(false); }} /> @@ -1340,6 +1631,70 @@
)} + {/* --- APPROVALS --- */} + {activeTab === 'approvals' && userRole === 'admin' && ( +
+
+

Aprovações de Moradores

+

Valide ou rejeite os novos pedidos de registo na plataforma.

+
+
+
+ + + + + + + + + + + + {residents.filter(r => r.status === 'pendente').map(req => ( + + + + + + + + ))} + {residents.filter(r => r.status === 'pendente').length === 0 && ( + + )} + +
MoradorNIF / CCContactoData Nasc.Ações
+

{req.name}

+

{req.email}

+
+

NIF: {req.nif}

+

CC: {req.cc}

+
{req.contact}{req.dob} +
+ + +
+
Nenhum pedido pendente de aprovação.
+
+
+
+ )} + {/* --- RESIDENTS --- */} {activeTab === 'residents' && (
@@ -1587,7 +1942,10 @@
-
+
setActiveChat({ type: 'global', id: 'global', name: 'Fórum do Condomínio' })} + className={`p-3 border-b-2 cursor-pointer transition-colors ${activeChat.type === 'global' ? 'border-blue-100 dark:border-blue-900/30 bg-blue-50/50 dark:bg-blue-900/10 hover:bg-blue-50/80 dark:hover:bg-blue-900/20' : 'border-transparent hover:bg-slate-50 dark:hover:bg-dark-card'}`} + >
@@ -1595,24 +1953,28 @@

Fórum do Condomínio

- Agora + Geral
-

Administração: Reunião na próxima sexta.

+

Grupo partilhado

- {residents.slice(0, 4).map(res => ( -
+ {residents.filter(r => r.id !== currentUserId).map(res => ( +
setActiveChat({ type: 'private', id: res.id, name: res.name })} + className={`p-3 border-b border-slate-50 dark:border-dark-border/50 cursor-pointer transition-colors ${activeChat.id === res.id ? 'bg-blue-50/50 dark:bg-blue-900/10' : 'hover:bg-slate-50 dark:hover:bg-dark-card'}`} + >
{res.name.substring(0, 2).toUpperCase()}
-

{res.name} ({res.unit})

- Ontem +

{res.name} {res.unit && `(${res.unit})`}

+ {activeChat.id === res.id && }
-

Tudo bem, tratamos disso!

+

Morador

@@ -1624,55 +1986,75 @@
-
- +
+ {activeChat.type === 'global' ? : activeChat.name.substring(0, 2).toUpperCase()}
-

Fórum do Condomínio

-

Todos os moradores

+

{activeChat.name}

+

{activeChat.type === 'global' ? 'Todos os moradores' : 'Privado'}

-
-
Hoje
+
+
Mensagens
-
-
-

Administração

-

Bom dia a todos. Relembramos que a manutenção dos elevadores ocorrerá amanhã às 10h.

- 09:00 -
-
- -
-
-

Obrigado pelo aviso!

- 09:15 -
-
- -
-
-

Maria Pereira (2º Esq)

-

Alguém encontrou um porta-chaves com formato de gato na entrada do prédio?

- 11:32 -
-
+ {messages.map((msg) => { + const isMe = msg.senderId === currentUserId; + const timeString = new Date(msg.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); + + return ( +
+
+ {!isMe && ( +

+ {msg.senderName} +

+ )} +

{msg.text}

+ {timeString} +
+
+ ); + })}
-
{ e.preventDefault(); showNotification('A sua mensagem foi enviada!', 'success'); e.target.reset(); }} className="flex gap-2"> + { + e.preventDefault(); + if (!newMessageText.trim()) return; + + try { + const path = activeChat.type === 'global' + ? 'mural_mensagens' + : `mensagens_privadas/${[currentUserId, activeChat.id].sort().join('_')}`; + + const newMsgRef = push(ref(db, path)); + await set(newMsgRef, { + text: newMessageText, + senderId: currentUserId, + senderName: currentUserName, + role: userRole, + timestamp: Date.now() + }); + setNewMessageText(''); + } catch (error) { + console.error("Erro ao enviar mensagem:", error); + showNotification("Erro ao enviar mensagem.", "error"); + } + }} className="flex gap-2"> setNewMessageText(e.target.value)} placeholder="Escreva a sua mensagem..." className="flex-1 bg-slate-50 dark:bg-dark-bg border border-slate-200 dark:border-dark-border rounded-full px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:text-white" /> -