ajusteLogin
This commit is contained in:
117
README.md
117
README.md
@@ -1,78 +1,47 @@
|
|||||||
# CondoMaster Pro
|
# CondoMasterResults
|
||||||
|
|
||||||

|
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).*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
*Construído com simplicidade e foco na comunidade.*
|
||||||
## ✨ 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.***
|
|
||||||
|
|||||||
464
index.html
464
index.html
@@ -99,7 +99,7 @@
|
|||||||
MessageCircle, Paperclip, Send
|
MessageCircle, Paperclip, Send
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { app } from './firebase.js';
|
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';
|
import { getDatabase, ref, push, set, onValue, remove } from 'https://www.gstatic.com/firebasejs/12.1.0/firebase-database.js';
|
||||||
|
|
||||||
const auth = getAuth(app);
|
const auth = getAuth(app);
|
||||||
@@ -257,7 +257,119 @@
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const LoginView = ({ onLogin }) => {
|
const WaitingApprovalView = ({ onLogout }) => {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-slate-100 dark:bg-dark-bg p-4 font-sans text-center">
|
||||||
|
<div className="bg-white dark:bg-dark-surface p-8 rounded-xl shadow-2xl w-full max-w-md border border-slate-100 dark:border-dark-border">
|
||||||
|
<div className="inline-flex p-4 bg-orange-100 dark:bg-orange-900/30 rounded-full mb-6 text-orange-600 dark:text-orange-400">
|
||||||
|
<AlertCircle size={48} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-slate-800 dark:text-white mb-2">Conta Pendente</h2>
|
||||||
|
<p className="text-slate-500 dark:text-gray-400 mb-8">
|
||||||
|
O seu registo foi concluído com sucesso, mas a sua conta aguarda aprovação da administração. Por favor, aguarde.
|
||||||
|
</p>
|
||||||
|
<button onClick={onLogout} className="w-full bg-slate-200 hover:bg-slate-300 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-800 dark:text-white font-bold py-3 rounded-lg transition-colors">
|
||||||
|
Sair
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-slate-100 dark:bg-dark-bg p-4 py-12 transition-colors duration-300 font-sans">
|
||||||
|
<div className="bg-white dark:bg-dark-surface p-8 rounded-xl shadow-2xl w-full max-w-2xl animate-fade-in border border-slate-100 dark:border-dark-border">
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-2xl font-bold text-slate-800 dark:text-white">Criar Conta</h1>
|
||||||
|
<p className="text-slate-500 dark:text-gray-400 mt-2">Registo de Novo Morador</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="col-span-1 md:col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Nome Completo</label>
|
||||||
|
<input type="text" name="name" value={formData.name} onChange={handleChange} required className="w-full px-4 py-2 rounded-lg border border-slate-300 dark:border-dark-border focus:ring-2 focus:ring-blue-500 bg-white dark:bg-dark-card text-slate-900 dark:text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Data de Nascimento</label>
|
||||||
|
<input type="date" name="dob" value={formData.dob} onChange={handleChange} required className="w-full px-4 py-2 rounded-lg border border-slate-300 dark:border-dark-border focus:ring-2 focus:ring-blue-500 bg-white dark:bg-dark-card text-slate-900 dark:text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Nº Telemóvel</label>
|
||||||
|
<input type="tel" name="phone" value={formData.phone} onChange={handleChange} required className="w-full px-4 py-2 rounded-lg border border-slate-300 dark:border-dark-border focus:ring-2 focus:ring-blue-500 bg-white dark:bg-dark-card text-slate-900 dark:text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Cartão de Cidadão</label>
|
||||||
|
<input type="text" name="cc" value={formData.cc} onChange={handleChange} required className="w-full px-4 py-2 rounded-lg border border-slate-300 dark:border-dark-border focus:ring-2 focus:ring-blue-500 bg-white dark:bg-dark-card text-slate-900 dark:text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">NIF</label>
|
||||||
|
<input type="text" name="nif" value={formData.nif} onChange={handleChange} required className="w-full px-4 py-2 rounded-lg border border-slate-300 dark:border-dark-border focus:ring-2 focus:ring-blue-500 bg-white dark:bg-dark-card text-slate-900 dark:text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Fração (Ex: 1º Dto)</label>
|
||||||
|
<input type="text" name="unit" value={formData.unit} onChange={handleChange} required className="w-full px-4 py-2 rounded-lg border border-slate-300 dark:border-dark-border focus:ring-2 focus:ring-blue-500 bg-white dark:bg-dark-card text-slate-900 dark:text-white" />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1 md:col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Email</label>
|
||||||
|
<input type="email" name="email" value={formData.email} onChange={handleChange} required className="w-full px-4 py-2 rounded-lg border border-slate-300 dark:border-dark-border focus:ring-2 focus:ring-blue-500 bg-white dark:bg-dark-card text-slate-900 dark:text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Palavra-passe</label>
|
||||||
|
<input type="password" name="password" value={formData.password} onChange={handleChange} required className="w-full px-4 py-2 rounded-lg border border-slate-300 dark:border-dark-border focus:ring-2 focus:ring-blue-500 bg-white dark:bg-dark-card text-slate-900 dark:text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Confirmar Palavra-passe</label>
|
||||||
|
<input type="password" name="confirmPassword" value={formData.confirmPassword} onChange={handleChange} required className="w-full px-4 py-2 rounded-lg border border-slate-300 dark:border-dark-border focus:ring-2 focus:ring-blue-500 bg-white dark:bg-dark-card text-slate-900 dark:text-white" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="col-span-1 md:col-span-2 p-3 bg-red-50 text-red-600 text-sm rounded-lg flex items-center gap-2">
|
||||||
|
<AlertCircle size={16} />
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="col-span-1 md:col-span-2 mt-4 space-y-4">
|
||||||
|
<button type="submit" disabled={!isFormValid} className="w-full bg-blue-600 disabled:bg-blue-300 hover:bg-blue-700 text-white font-bold py-3 rounded-lg transition-colors">
|
||||||
|
Registar
|
||||||
|
</button>
|
||||||
|
<div className="text-center">
|
||||||
|
<button type="button" onClick={onToggleView} className="text-sm text-blue-600 hover:underline dark:text-blue-400">
|
||||||
|
Já tem conta? Iniciar Sessão
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LoginView = ({ onLogin, onToggleView }) => {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@@ -311,9 +423,16 @@
|
|||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="space-y-4">
|
||||||
<button type="submit" className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded-lg transition-colors shadow-lg shadow-blue-500/30">
|
<button type="submit" className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded-lg transition-colors shadow-lg shadow-blue-500/30">
|
||||||
Entrar
|
Entrar
|
||||||
</button>
|
</button>
|
||||||
|
<div className="text-center">
|
||||||
|
<button type="button" onClick={onToggleView} className="text-sm text-blue-600 hover:underline dark:text-blue-400">
|
||||||
|
Ainda não tem conta? Registe-se
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -332,43 +451,171 @@
|
|||||||
const [userRole, setUserRole] = useState(() => {
|
const [userRole, setUserRole] = useState(() => {
|
||||||
return sessionStorage.getItem('condo_role') || 'morador';
|
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) => {
|
const handleLogin = async (email, password) => {
|
||||||
try {
|
try {
|
||||||
const userCredential = await signInWithEmailAndPassword(auth, email, password);
|
const userCredential = await signInWithEmailAndPassword(auth, email, password);
|
||||||
let role = 'morador';
|
let role = 'morador';
|
||||||
|
let userName = 'Utilizador';
|
||||||
|
let userId = userCredential.user.uid;
|
||||||
|
let status = 'aprovado';
|
||||||
|
|
||||||
if (email.toLowerCase().includes('admin')) {
|
if (email.toLowerCase().includes('admin')) {
|
||||||
role = 'admin';
|
role = 'admin';
|
||||||
|
userName = 'Administração';
|
||||||
} else {
|
} 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) {
|
if (residentUser) {
|
||||||
role = residentUser.role || 'morador';
|
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_auth', 'true');
|
||||||
sessionStorage.setItem('condo_role', role);
|
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);
|
setIsAuthenticated(true);
|
||||||
setUserRole(role);
|
setUserRole(role);
|
||||||
|
setCurrentUserName(userName);
|
||||||
|
setCurrentUserId(userId);
|
||||||
|
setUserStatus(status);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Firebase Auth falhou, a tentar conta local...", error);
|
console.log("Firebase Auth falhou, a tentar conta local...", error);
|
||||||
let role = null;
|
let role = null;
|
||||||
|
let userName = 'Utilizador';
|
||||||
|
let userId = 'local_' + Date.now();
|
||||||
|
|
||||||
|
let status = 'aprovado';
|
||||||
|
|
||||||
if (email === 'administradores@gmail.com' && password === 'admin123') {
|
if (email === 'administradores@gmail.com' && password === 'admin123') {
|
||||||
role = 'admin';
|
role = 'admin';
|
||||||
} else if (email === 'moradores@gmail.com' && password === 'moradores123') {
|
userName = 'Administração';
|
||||||
role = 'morador';
|
userId = 'admin_001';
|
||||||
} else {
|
} else {
|
||||||
const residentUser = residents.find(r => r.email && r.email.toLowerCase() === email.toLowerCase());
|
const residentUser = residents.find(r => r.email && r.email.toLowerCase() === email.toLowerCase());
|
||||||
if (residentUser && (password === residentUser.contact || password === '1234')) {
|
if (residentUser && (password === residentUser.contact || password === '1234')) {
|
||||||
role = residentUser.role || 'morador';
|
role = residentUser.role || 'morador';
|
||||||
|
userName = residentUser.name + (residentUser.unit && residentUser.unit !== 'Pendente' ? ` (${residentUser.unit})` : '');
|
||||||
|
userId = residentUser.id || userId;
|
||||||
|
status = residentUser.status || 'aprovado';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role) {
|
if (role) {
|
||||||
sessionStorage.setItem('condo_auth', 'true');
|
sessionStorage.setItem('condo_auth', 'true');
|
||||||
sessionStorage.setItem('condo_role', role);
|
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);
|
setIsAuthenticated(true);
|
||||||
setUserRole(role);
|
setUserRole(role);
|
||||||
|
setCurrentUserName(userName);
|
||||||
|
setCurrentUserId(userId);
|
||||||
|
setUserStatus(status);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -379,8 +626,14 @@
|
|||||||
if (window.confirm('Tem a certeza que deseja terminar sessão?')) {
|
if (window.confirm('Tem a certeza que deseja terminar sessão?')) {
|
||||||
sessionStorage.removeItem('condo_auth');
|
sessionStorage.removeItem('condo_auth');
|
||||||
sessionStorage.removeItem('condo_role');
|
sessionStorage.removeItem('condo_role');
|
||||||
|
sessionStorage.removeItem('condo_user_name');
|
||||||
|
sessionStorage.removeItem('condo_user_id');
|
||||||
|
sessionStorage.removeItem('condo_user_status');
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
setUserRole(null);
|
setUserRole(null);
|
||||||
|
setCurrentUserName('Utilizador');
|
||||||
|
setCurrentUserId('0');
|
||||||
|
setUserStatus('aprovado');
|
||||||
setActiveTab('dashboard');
|
setActiveTab('dashboard');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -390,6 +643,9 @@
|
|||||||
const [issues, setIssues] = useState([]);
|
const [issues, setIssues] = useState([]);
|
||||||
const [bookings, setBookings] = useState([]);
|
const [bookings, setBookings] = useState([]);
|
||||||
const [invoices, setInvoices] = 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(() => {
|
useEffect(() => {
|
||||||
const loadData = (path, setter, sortFunc = null) => {
|
const loadData = (path, setter, sortFunc = null) => {
|
||||||
@@ -415,6 +671,26 @@
|
|||||||
unsubResidents(); unsubFinances(); unsubIssues(); unsubBookings(); unsubInvoices();
|
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 [notificationsList, setNotificationsList] = useState(INITIAL_NOTIFICATIONS);
|
||||||
const [isNotificationsOpen, setNotificationsOpen] = useState(false);
|
const [isNotificationsOpen, setNotificationsOpen] = useState(false);
|
||||||
|
|
||||||
@@ -881,12 +1157,20 @@
|
|||||||
<h3 className="font-bold text-lg text-slate-800 dark:text-white">Manutenção e Ocorrências</h3>
|
<h3 className="font-bold text-lg text-slate-800 dark:text-white">Manutenção e Ocorrências</h3>
|
||||||
<p className="text-sm text-slate-500 dark:text-dark-mute">Gestão de pedidos e reparações</p>
|
<p className="text-sm text-slate-500 dark:text-dark-mute">Gestão de pedidos e reparações</p>
|
||||||
</div>
|
</div>
|
||||||
|
{userRole === 'admin' ? (
|
||||||
|
<button
|
||||||
|
className="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 shadow-sm transition-colors cursor-default"
|
||||||
|
>
|
||||||
|
<CheckCircle size={18} /> Resolver Problemas
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleOpenModal('issue')}
|
onClick={() => handleOpenModal('issue')}
|
||||||
className="bg-orange-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-orange-700 flex items-center gap-2 shadow-sm transition-colors"
|
className="bg-orange-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-orange-700 flex items-center gap-2 shadow-sm transition-colors"
|
||||||
>
|
>
|
||||||
<Plus size={18} /> Reportar Problema
|
<Plus size={18} /> Reportar Problema
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-auto p-6 bg-slate-50 dark:bg-dark-bg">
|
<div className="flex-1 overflow-auto p-6 bg-slate-50 dark:bg-dark-bg">
|
||||||
@@ -1117,7 +1401,13 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return <LoginView onLogin={handleLogin} />;
|
return authView === 'login'
|
||||||
|
? <LoginView onLogin={handleLogin} onToggleView={() => setAuthView('register')} />
|
||||||
|
: <RegisterView onRegister={handleRegister} onToggleView={() => setAuthView('login')} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userStatus === 'pendente') {
|
||||||
|
return <WaitingApprovalView onLogout={handleLogout} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1149,6 +1439,7 @@
|
|||||||
{userRole === 'admin' && <SidebarItem icon={Users} label="Condóminos" active={activeTab === 'residents'} onClick={() => { setActiveTab('residents'); setSidebarOpen(false); }} />}
|
{userRole === 'admin' && <SidebarItem icon={Users} label="Condóminos" active={activeTab === 'residents'} onClick={() => { setActiveTab('residents'); setSidebarOpen(false); }} />}
|
||||||
{userRole === 'admin' && <SidebarItem icon={Wallet} label="Finanças" active={activeTab === 'finance'} onClick={() => { setActiveTab('finance'); setSidebarOpen(false); }} />}
|
{userRole === 'admin' && <SidebarItem icon={Wallet} label="Finanças" active={activeTab === 'finance'} onClick={() => { setActiveTab('finance'); setSidebarOpen(false); }} />}
|
||||||
{userRole === 'admin' && <SidebarItem icon={FileText} label="Faturação" active={activeTab === 'billing'} onClick={() => { setActiveTab('billing'); setSidebarOpen(false); }} />}
|
{userRole === 'admin' && <SidebarItem icon={FileText} label="Faturação" active={activeTab === 'billing'} onClick={() => { setActiveTab('billing'); setSidebarOpen(false); }} />}
|
||||||
|
{userRole === 'admin' && <SidebarItem icon={Users} label="Aprovações" active={activeTab === 'approvals'} onClick={() => { setActiveTab('approvals'); setSidebarOpen(false); }} />}
|
||||||
<SidebarItem icon={Wrench} label="Manutenção" active={activeTab === 'maintenance'} onClick={() => { setActiveTab('maintenance'); setSidebarOpen(false); }} />
|
<SidebarItem icon={Wrench} label="Manutenção" active={activeTab === 'maintenance'} onClick={() => { setActiveTab('maintenance'); setSidebarOpen(false); }} />
|
||||||
<SidebarItem icon={MessageCircle} label="Mensagens" active={activeTab === 'messages'} onClick={() => { setActiveTab('messages'); setSidebarOpen(false); }} />
|
<SidebarItem icon={MessageCircle} label="Mensagens" active={activeTab === 'messages'} onClick={() => { setActiveTab('messages'); setSidebarOpen(false); }} />
|
||||||
<SidebarItem icon={Map} label="Mapa" active={activeTab === 'map'} onClick={() => { setActiveTab('map'); setSidebarOpen(false); }} />
|
<SidebarItem icon={Map} label="Mapa" active={activeTab === 'map'} onClick={() => { setActiveTab('map'); setSidebarOpen(false); }} />
|
||||||
@@ -1340,6 +1631,70 @@
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* --- APPROVALS --- */}
|
||||||
|
{activeTab === 'approvals' && userRole === 'admin' && (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="mb-6">
|
||||||
|
<h2 className="text-2xl font-bold text-slate-800 dark:text-white">Aprovações de Moradores</h2>
|
||||||
|
<p className="text-slate-500 dark:text-dark-mute">Valide ou rejeite os novos pedidos de registo na plataforma.</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border overflow-hidden">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-left border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-slate-50 dark:bg-dark-bg border-b border-slate-100 dark:border-dark-border text-sm font-semibold text-slate-500 dark:text-slate-400">
|
||||||
|
<th className="p-4">Morador</th>
|
||||||
|
<th className="p-4">NIF / CC</th>
|
||||||
|
<th className="p-4">Contacto</th>
|
||||||
|
<th className="p-4">Data Nasc.</th>
|
||||||
|
<th className="p-4 text-center">Ações</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{residents.filter(r => r.status === 'pendente').map(req => (
|
||||||
|
<tr key={req.id} className="border-b border-slate-50 dark:border-dark-border hover:bg-slate-50/50 dark:hover:bg-dark-bg/50">
|
||||||
|
<td className="p-4">
|
||||||
|
<p className="font-semibold text-slate-700 dark:text-slate-200">{req.name}</p>
|
||||||
|
<p className="text-xs text-slate-400">{req.email}</p>
|
||||||
|
</td>
|
||||||
|
<td className="p-4 text-slate-600 dark:text-slate-400">
|
||||||
|
<p className="text-sm">NIF: {req.nif}</p>
|
||||||
|
<p className="text-sm">CC: {req.cc}</p>
|
||||||
|
</td>
|
||||||
|
<td className="p-4 text-sm text-slate-600 dark:text-slate-400">{req.contact}</td>
|
||||||
|
<td className="p-4 text-sm text-slate-600 dark:text-slate-400">{req.dob}</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<div className="flex justify-center gap-2">
|
||||||
|
<button onClick={() => {
|
||||||
|
if(window.confirm('Aprovar este morador?')) {
|
||||||
|
set(ref(db, `condominos/${req.id}/status`), 'aprovado');
|
||||||
|
showNotification('Morador aprovado com sucesso!', 'success');
|
||||||
|
}
|
||||||
|
}} className="p-2 bg-green-100 text-green-600 rounded hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400" title="Aprovar">
|
||||||
|
<CheckCircle size={18} />
|
||||||
|
</button>
|
||||||
|
<button onClick={() => {
|
||||||
|
if(window.confirm('Rejeitar este pedido? O registo será eliminado.')) {
|
||||||
|
remove(ref(db, `condominos/${req.id}`));
|
||||||
|
showNotification('Registo eliminado.', 'success');
|
||||||
|
}
|
||||||
|
}} className="p-2 bg-red-100 text-red-600 rounded hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400" title="Rejeitar">
|
||||||
|
<Trash2 size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{residents.filter(r => r.status === 'pendente').length === 0 && (
|
||||||
|
<tr><td colSpan="5" className="p-8 text-center text-slate-500">Nenhum pedido pendente de aprovação.</td></tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* --- RESIDENTS --- */}
|
{/* --- RESIDENTS --- */}
|
||||||
{activeTab === 'residents' && (
|
{activeTab === 'residents' && (
|
||||||
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border overflow-hidden animate-fade-in flex flex-col h-full transition-colors">
|
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border overflow-hidden animate-fade-in flex flex-col h-full transition-colors">
|
||||||
@@ -1587,7 +1942,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<div className="p-3 border-b-2 border-blue-100 dark:border-blue-900/30 bg-blue-50/50 dark:bg-blue-900/10 cursor-pointer hover:bg-slate-50 dark:hover:bg-dark-card transition-colors">
|
<div
|
||||||
|
onClick={() => 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'}`}
|
||||||
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center text-blue-600 dark:text-blue-400 font-bold shrink-0">
|
<div className="w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center text-blue-600 dark:text-blue-400 font-bold shrink-0">
|
||||||
<Users size={20} />
|
<Users size={20} />
|
||||||
@@ -1595,24 +1953,28 @@
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex justify-between items-baseline mb-0.5">
|
<div className="flex justify-between items-baseline mb-0.5">
|
||||||
<h4 className="text-sm font-bold text-slate-800 dark:text-slate-200 truncate">Fórum do Condomínio</h4>
|
<h4 className="text-sm font-bold text-slate-800 dark:text-slate-200 truncate">Fórum do Condomínio</h4>
|
||||||
<span className="text-xs text-blue-600 dark:text-blue-400 font-medium whitespace-nowrap">Agora</span>
|
<span className="text-xs text-blue-600 dark:text-blue-400 font-medium whitespace-nowrap">Geral</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400 truncate">Administração: Reunião na próxima sexta.</p>
|
<p className="text-xs text-slate-500 dark:text-slate-400 truncate">Grupo partilhado</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{residents.slice(0, 4).map(res => (
|
{residents.filter(r => r.id !== currentUserId).map(res => (
|
||||||
<div key={res.id} className="p-3 border-b border-slate-50 dark:border-dark-border/50 cursor-pointer hover:bg-slate-50 dark:hover:bg-dark-card transition-colors">
|
<div
|
||||||
|
key={res.id}
|
||||||
|
onClick={() => 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'}`}
|
||||||
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center text-slate-600 dark:text-slate-300 font-bold shrink-0 text-sm">
|
<div className="w-10 h-10 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center text-slate-600 dark:text-slate-300 font-bold shrink-0 text-sm">
|
||||||
{res.name.substring(0, 2).toUpperCase()}
|
{res.name.substring(0, 2).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex justify-between items-baseline mb-0.5">
|
<div className="flex justify-between items-baseline mb-0.5">
|
||||||
<h4 className="text-sm font-medium text-slate-700 dark:text-slate-300 truncate">{res.name} ({res.unit})</h4>
|
<h4 className="text-sm font-medium text-slate-700 dark:text-slate-300 truncate">{res.name} {res.unit && `(${res.unit})`}</h4>
|
||||||
<span className="text-xs text-slate-400 whitespace-nowrap">Ontem</span>
|
{activeChat.id === res.id && <span className="w-2 h-2 rounded-full bg-blue-500"></span>}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-500 truncate">Tudo bem, tratamos disso!</p>
|
<p className="text-xs text-slate-500 dark:text-slate-500 truncate">Morador</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1624,55 +1986,75 @@
|
|||||||
<div className="flex-1 flex flex-col h-full bg-slate-50/50 dark:bg-dark-bg/50">
|
<div className="flex-1 flex flex-col h-full bg-slate-50/50 dark:bg-dark-bg/50">
|
||||||
<div className="p-4 border-b border-slate-100 dark:border-dark-border bg-white dark:bg-dark-surface flex items-center justify-between">
|
<div className="p-4 border-b border-slate-100 dark:border-dark-border bg-white dark:bg-dark-surface flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center text-blue-600 dark:text-blue-400 font-bold">
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-bold ${activeChat.type === 'global' ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400' : 'bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-300'}`}>
|
||||||
<Users size={20} />
|
{activeChat.type === 'global' ? <Users size={20} /> : activeChat.name.substring(0, 2).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-bold text-slate-800 dark:text-white">Fórum do Condomínio</h3>
|
<h3 className="font-bold text-slate-800 dark:text-white">{activeChat.name}</h3>
|
||||||
<p className="text-xs text-green-500 font-medium">Todos os moradores</p>
|
<p className="text-xs text-green-500 font-medium">{activeChat.type === 'global' ? 'Todos os moradores' : 'Privado'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200"><Info size={20}/></button>
|
<button className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200"><Info size={20}/></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
<div className="flex-1 overflow-y-auto p-4 space-y-4 flex flex-col">
|
||||||
<div className="text-center my-4"><span className="text-xs bg-slate-200/50 dark:bg-slate-800 text-slate-500 px-3 py-1 rounded-full">Hoje</span></div>
|
<div className="text-center my-4"><span className="text-xs bg-slate-200/50 dark:bg-slate-800 text-slate-500 px-3 py-1 rounded-full">Mensagens</span></div>
|
||||||
|
|
||||||
<div className="flex justify-start">
|
{messages.map((msg) => {
|
||||||
<div className="bg-white dark:bg-dark-card border border-slate-100 dark:border-slate-800 text-slate-700 dark:text-slate-200 p-3 rounded-2xl rounded-tl-sm max-w-[80%] shadow-sm">
|
const isMe = msg.senderId === currentUserId;
|
||||||
<p className="text-xs text-blue-600 dark:text-blue-400 font-bold mb-1">Administração</p>
|
const timeString = new Date(msg.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
||||||
<p className="text-sm">Bom dia a todos. Relembramos que a manutenção dos elevadores ocorrerá amanhã às 10h.</p>
|
|
||||||
<span className="text-[10px] text-slate-400 block text-right mt-1">09:00</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end">
|
return (
|
||||||
<div className="bg-blue-600 text-white p-3 rounded-2xl rounded-tr-sm max-w-[80%] shadow-sm">
|
<div key={msg.id} className={`flex ${isMe ? 'justify-end' : 'justify-start'}`}>
|
||||||
<p className="text-sm">Obrigado pelo aviso!</p>
|
<div className={`${isMe ? 'bg-blue-600 text-white rounded-2xl rounded-tr-sm' : 'bg-white dark:bg-dark-card border border-slate-100 dark:border-slate-800 text-slate-700 dark:text-slate-200 rounded-2xl rounded-tl-sm'} p-3 max-w-[80%] shadow-sm`}>
|
||||||
<span className="text-[10px] text-blue-200 block text-right mt-1">09:15</span>
|
{!isMe && (
|
||||||
</div>
|
<p className={`text-xs ${msg.role === 'admin' ? 'text-blue-600 dark:text-blue-400' : 'text-orange-600 dark:text-orange-400'} font-bold mb-1`}>
|
||||||
</div>
|
{msg.senderName}
|
||||||
|
</p>
|
||||||
<div className="flex justify-start">
|
)}
|
||||||
<div className="bg-white dark:bg-dark-card border border-slate-100 dark:border-slate-800 text-slate-700 dark:text-slate-200 p-3 rounded-2xl rounded-tl-sm max-w-[80%] shadow-sm">
|
<p className="text-sm">{msg.text}</p>
|
||||||
<p className="text-xs text-orange-600 dark:text-orange-400 font-bold mb-1">Maria Pereira (2º Esq)</p>
|
<span className={`text-[10px] ${isMe ? 'text-blue-200' : 'text-slate-400'} block text-right mt-1`}>{timeString}</span>
|
||||||
<p className="text-sm">Alguém encontrou um porta-chaves com formato de gato na entrada do prédio?</p>
|
|
||||||
<span className="text-[10px] text-slate-400 block text-right mt-1">11:32</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 bg-white dark:bg-dark-surface border-t border-slate-100 dark:border-dark-border">
|
<div className="p-4 bg-white dark:bg-dark-surface border-t border-slate-100 dark:border-dark-border">
|
||||||
<form onSubmit={(e) => { e.preventDefault(); showNotification('A sua mensagem foi enviada!', 'success'); e.target.reset(); }} className="flex gap-2">
|
<form onSubmit={async (e) => {
|
||||||
|
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">
|
||||||
<button type="button" className="p-2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors hidden sm:block">
|
<button type="button" className="p-2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors hidden sm:block">
|
||||||
<Paperclip size={20} />
|
<Paperclip size={20} />
|
||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
value={newMessageText}
|
||||||
|
onChange={(e) => setNewMessageText(e.target.value)}
|
||||||
placeholder="Escreva a sua mensagem..."
|
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"
|
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"
|
||||||
/>
|
/>
|
||||||
<button type="submit" className="p-2 bg-blue-600 text-white rounded-full hover:bg-blue-700 transition-colors shadow-sm flex items-center justify-center shrink-0 w-10 h-10">
|
<button type="submit" disabled={!newMessageText.trim()} className="p-2 bg-blue-600 text-white rounded-full hover:bg-blue-700 disabled:bg-blue-400 transition-colors shadow-sm flex items-center justify-center shrink-0 w-10 h-10">
|
||||||
<Send size={18} />
|
<Send size={18} />
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user