ajuste tela login
This commit is contained in:
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"liveServer.settings.port": 5501
|
||||
"liveServer.settings.port": 5502
|
||||
}
|
||||
79
README.md
79
README.md
@@ -1 +1,78 @@
|
||||
# GestorCondominio
|
||||
# CondoMaster Pro
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## ✨ 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.***
|
||||
|
||||
326
index.html
326
index.html
@@ -100,11 +100,11 @@
|
||||
|
||||
|
||||
const INITIAL_RESIDENTS = [
|
||||
{ id: 1, unit: '1º Esq', name: 'Ana Silva', contact: '912 345 678', email: 'ana.silva@email.com', status: 'Pago', pending: 0 },
|
||||
{ id: 2, unit: '1º Dto', name: 'Carlos Santos', contact: '965 432 109', email: 'carlos.s@email.com', status: 'Pendente', pending: 45.00 },
|
||||
{ id: 3, unit: '2º Esq', name: 'Maria Pereira', contact: '933 221 110', email: 'maria.p@email.com', status: 'Pago', pending: 0 },
|
||||
{ id: 4, unit: '2º Dto', name: 'João Ferreira', contact: '918 765 432', email: 'joao.f@email.com', status: 'Atrasado', pending: 135.00 },
|
||||
{ id: 5, unit: '3º Esq', name: 'Sofia Costa', contact: '922 334 455', email: 'sofia.c@email.com', status: 'Pago', pending: 0 },
|
||||
{ id: 1, unit: '1º Esq', name: 'Ana Silva', contact: '912 345 678', email: 'ana.silva@email.com', status: 'Pago', pending: 0, role: 'morador' },
|
||||
{ id: 2, unit: '1º Dto', name: 'Carlos Santos', contact: '965 432 109', email: 'carlos.s@email.com', status: 'Pendente', pending: 45.00, role: 'morador' },
|
||||
{ id: 3, unit: '2º Esq', name: 'Maria Pereira', contact: '933 221 110', email: 'maria.p@email.com', status: 'Pago', pending: 0, role: 'morador' },
|
||||
{ id: 4, unit: '2º Dto', name: 'João Ferreira', contact: '918 765 432', email: 'joao.f@email.com', status: 'Atrasado', pending: 135.00, role: 'morador' },
|
||||
{ id: 5, unit: '3º Esq', name: 'Sofia Costa', contact: '922 334 455', email: 'sofia.c@email.com', status: 'Pago', pending: 0, role: 'morador' },
|
||||
];
|
||||
|
||||
const INITIAL_FINANCES = [
|
||||
@@ -258,12 +258,9 @@
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (email === 'admin@gmail.com' && password === 'admin123') {
|
||||
onLogin({ role: 'admin', email });
|
||||
} else if (email === 'moradores@gmail.com' && password === 'moradores123') {
|
||||
onLogin({ role: 'resident', email });
|
||||
} else {
|
||||
setError('Credenciais incorretas');
|
||||
const success = onLogin(email, password);
|
||||
if (!success) {
|
||||
setError('Email ou Palavra-passe incorreta');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -280,13 +277,13 @@
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">E-mail</label>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full px-4 py-3 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 transition-colors"
|
||||
placeholder="Endereço de e-mail"
|
||||
placeholder="Endereço de email"
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
@@ -327,16 +324,30 @@
|
||||
return sessionStorage.getItem('condo_auth') === 'true';
|
||||
});
|
||||
const [userRole, setUserRole] = useState(() => {
|
||||
return sessionStorage.getItem('condo_role') || 'admin';
|
||||
return sessionStorage.getItem('condo_role') || 'morador';
|
||||
});
|
||||
|
||||
const handleLogin = (userData) => {
|
||||
sessionStorage.setItem('condo_auth', 'true');
|
||||
if (userData && userData.role) {
|
||||
sessionStorage.setItem('condo_role', userData.role);
|
||||
setUserRole(userData.role);
|
||||
const handleLogin = (email, password) => {
|
||||
let role = null;
|
||||
if (email === 'administradores@gmail.com' && password === 'admin123') {
|
||||
role = 'admin';
|
||||
} else if (email === 'moradores@gmail.com' && password === 'moradores123') {
|
||||
role = 'morador';
|
||||
} else {
|
||||
const residentUser = residents.find(r => r.email && r.email.toLowerCase() === email.toLowerCase());
|
||||
if (residentUser && (password === residentUser.contact || password === '1234')) {
|
||||
role = residentUser.role || 'morador';
|
||||
}
|
||||
}
|
||||
|
||||
if (role) {
|
||||
sessionStorage.setItem('condo_auth', 'true');
|
||||
sessionStorage.setItem('condo_role', role);
|
||||
setIsAuthenticated(true);
|
||||
setUserRole(role);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
@@ -344,6 +355,7 @@
|
||||
sessionStorage.removeItem('condo_auth');
|
||||
sessionStorage.removeItem('condo_role');
|
||||
setIsAuthenticated(false);
|
||||
setUserRole(null);
|
||||
setActiveTab('dashboard');
|
||||
}
|
||||
};
|
||||
@@ -362,7 +374,7 @@
|
||||
|
||||
const notificationRef = useRef(null);
|
||||
|
||||
const initialResidentForm = { unit: '', name: '', contact: '', email: '', status: 'Pago', pending: 0 };
|
||||
const initialResidentForm = { unit: '', name: '', contact: '', email: '', status: 'Pago', pending: 0, role: 'morador' };
|
||||
const initialFinanceForm = { type: 'expense', category: '', amount: '', desc: '', date: new Date().toISOString().split('T')[0] };
|
||||
const initialIssueForm = { title: '', location: '', priority: 'Média', status: 'Novo', date: new Date().toISOString().split('T')[0] };
|
||||
const initialBookingForm = { facility: 'gym', date: new Date().toISOString().split('T')[0], time: '', resident: '', cost: 0 };
|
||||
@@ -462,6 +474,11 @@
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleRole = (id) => {
|
||||
setResidents(residents.map(r => r.id === id ? { ...r, role: r.role === 'admin' ? 'morador' : 'admin' } : r));
|
||||
showNotification('Permissões de utilizador atualizadas', 'success');
|
||||
};
|
||||
|
||||
const handleSaveResident = (e) => {
|
||||
e.preventDefault();
|
||||
if (editingItem) {
|
||||
@@ -533,7 +550,11 @@
|
||||
const DashboardView = () => (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{userRole === 'admin' ? (
|
||||
<Card title="Saldo Disponível" value={`${balance.toFixed(2)}€`} icon={Wallet} trend={balance >= 0 ? 'up' : 'down'} trendValue="Atual" color="bg-blue-500" />
|
||||
) : (
|
||||
<Card title="As Minhas Quotas" value="Em Dia" icon={CheckCircle} trend="up" trendValue="Pago" color="bg-green-500" subtitle="Sem valores pendentes" />
|
||||
)}
|
||||
<Card title="Reservas (Mês)" value={bookings.length} icon={Calendar} trend="up" trendValue="+2" color="bg-purple-500" subtitle="Total agendado" />
|
||||
<Card title="Manutenções Ativas" value={activeIssuesCount} icon={Wrench} trend="up" trendValue="Novas" color="bg-orange-500" subtitle="Em resolução" />
|
||||
</div>
|
||||
@@ -542,7 +563,7 @@
|
||||
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border flex flex-col h-full transition-colors">
|
||||
<div className="p-6 border-b border-slate-100 dark:border-dark-border flex justify-between items-center">
|
||||
<h3 className="font-bold text-lg text-slate-800 dark:text-white">Próximas Reservas</h3>
|
||||
<button className="text-blue-600 dark:text-blue-400 text-sm font-medium">Ver todas</button>
|
||||
<button className="text-blue-600 dark:text-blue-400 text-sm font-medium" onClick={() => setActiveTab('all_bookings')}>Ver todas as Reservas</button>
|
||||
</div>
|
||||
<div className="p-4 space-y-3">
|
||||
{bookings.slice(0, 4).map(booking => (
|
||||
@@ -568,7 +589,7 @@
|
||||
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border flex flex-col h-full transition-colors">
|
||||
<div className="p-6 border-b border-slate-100 dark:border-dark-border flex justify-between items-center">
|
||||
<h3 className="font-bold text-lg text-slate-800 dark:text-white">Quadro de Avisos</h3>
|
||||
<button className="text-sm text-blue-600 dark:text-blue-400" onClick={() => setActiveTab('maintenance')}>Gerir</button>
|
||||
<button className="text-sm text-blue-600 dark:text-blue-400" onClick={() => setActiveTab('maintenance')}>{userRole === 'admin' ? 'Gerir' : 'Ver Ocorrências'}</button>
|
||||
</div>
|
||||
<div className="p-4 space-y-3 overflow-y-auto max-h-[350px]">
|
||||
{issues.slice(0, 3).map((issue) => (
|
||||
@@ -752,7 +773,7 @@
|
||||
Prioridade {issue.priority}
|
||||
</span>
|
||||
|
||||
{issue.status !== 'Resolvido' && (
|
||||
{userRole === 'admin' && issue.status !== 'Resolvido' && (
|
||||
<button
|
||||
onClick={() => handleResolveIssue(issue.id)}
|
||||
className="text-sm text-green-600 dark:text-green-400 font-medium hover:text-green-700 dark:hover:text-green-300 flex items-center gap-1"
|
||||
@@ -779,10 +800,10 @@
|
||||
<div className="w-full md:w-64 bg-slate-50 dark:bg-dark-bg border-r border-slate-100 dark:border-dark-border p-6 flex flex-col gap-2">
|
||||
<div className="text-center mb-6">
|
||||
<div className="w-24 h-24 bg-blue-100 dark:bg-blue-900/30 rounded-full mx-auto flex items-center justify-center text-blue-600 dark:text-blue-400 font-bold text-3xl mb-3 border-4 border-white dark:border-dark-surface shadow-sm">
|
||||
AD
|
||||
{userRole === 'admin' ? 'AD' : 'MO'}
|
||||
</div>
|
||||
<h3 className="font-bold text-slate-800 dark:text-white">Admin Condomínio</h3>
|
||||
<p className="text-xs text-slate-500 dark:text-dark-mute">Administrador Geral</p>
|
||||
<h3 className="font-bold text-slate-800 dark:text-white">{userRole === 'admin' ? 'Admin Condomínio' : 'Morador'}</h3>
|
||||
<p className="text-xs text-slate-500 dark:text-dark-mute">{userRole === 'admin' ? 'Administrador Geral' : 'Residente'}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -797,12 +818,14 @@
|
||||
>
|
||||
<LogOut size={18} className="rotate-90" /> Segurança
|
||||
</button>
|
||||
{userRole === 'admin' && (
|
||||
<button
|
||||
onClick={() => setActiveSection('permissions')}
|
||||
className={`text-left px-4 py-3 rounded-lg text-sm font-medium transition-colors flex items-center gap-3 ${activeSection === 'permissions' ? 'bg-white dark:bg-dark-surface text-blue-600 dark:text-blue-400 shadow-sm' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-dark-card'}`}
|
||||
>
|
||||
<CheckCircle size={18} /> Permissões
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setActiveSection('settings')}
|
||||
className={`text-left px-4 py-3 rounded-lg text-sm font-medium transition-colors flex items-center gap-3 ${activeSection === 'settings' ? 'bg-white dark:bg-dark-surface text-blue-600 dark:text-blue-400 shadow-sm' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-dark-card'}`}
|
||||
@@ -827,7 +850,7 @@
|
||||
</div>
|
||||
<InputGroup label="Morada (Sede)" value="Rua das Flores, nº 123, Escritório 2B" />
|
||||
<div className="flex justify-end mt-6">
|
||||
<button className="bg-blue-600 text-white px-6 py-2 rounded-lg font-medium hover:bg-blue-700 shadow-sm transition-colors">
|
||||
<button onClick={() => showNotification('Alterações guardadas com sucesso!', 'success')} className="bg-blue-600 text-white px-6 py-2 rounded-lg font-medium hover:bg-blue-700 shadow-sm transition-colors">
|
||||
Guardar Alterações
|
||||
</button>
|
||||
</div>
|
||||
@@ -842,8 +865,8 @@
|
||||
<AlertCircle className="text-orange-600 dark:text-orange-400 flex-shrink-0 mt-0.5" size={20} />
|
||||
<div>
|
||||
<h4 className="font-bold text-orange-800 dark:text-orange-300 text-sm">Autenticação de Dois Fatores (2FA)</h4>
|
||||
<p className="text-xs text-orange-700 dark:text-orange-400 mt-1">Recomendamos ativar o 2FA para maior segurança da sua conta de administrador.</p>
|
||||
<button className="text-orange-900 dark:text-orange-200 text-xs font-bold underline mt-2">Ativar Agora</button>
|
||||
<p className="text-xs text-orange-700 dark:text-orange-400 mt-1">Recomendamos ativar o 2FA para maior segurança da sua conta.</p>
|
||||
<button onClick={() => showNotification('Autenticação de Dois Fatores ativada com sucesso!', 'success')} className="text-orange-900 dark:text-orange-200 text-xs font-bold underline mt-2">Ativar Agora</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -854,7 +877,7 @@
|
||||
<InputGroup label="Confirmar Nova Palavra-passe" type="password" placeholder="Confirmar" />
|
||||
</div>
|
||||
<div className="flex justify-end mt-6">
|
||||
<button className="bg-slate-800 dark:bg-slate-700 text-white px-6 py-2 rounded-lg font-medium hover:bg-slate-900 dark:hover:bg-slate-600 shadow-sm transition-colors">
|
||||
<button onClick={() => showNotification('Segurança atualizada com sucesso!', 'success')} className="bg-slate-800 dark:bg-slate-700 text-white px-6 py-2 rounded-lg font-medium hover:bg-slate-900 dark:hover:bg-slate-600 shadow-sm transition-colors">
|
||||
Atualizar Segurança
|
||||
</button>
|
||||
</div>
|
||||
@@ -976,13 +999,11 @@
|
||||
<div className="flex-1 px-4 py-2 space-y-1 overflow-y-auto">
|
||||
<div className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2 px-4 mt-2">Geral</div>
|
||||
<SidebarItem icon={Building2} label="Dashboard" active={activeTab === 'dashboard'} onClick={() => { setActiveTab('dashboard'); setSidebarOpen(false); }} />
|
||||
{userRole === 'admin' && (
|
||||
<>
|
||||
<SidebarItem icon={Users} label="Condóminos" active={activeTab === 'residents'} onClick={() => { setActiveTab('residents'); setSidebarOpen(false); }} />
|
||||
<SidebarItem icon={Wallet} label="Finanças" active={activeTab === 'finance'} onClick={() => { setActiveTab('finance'); 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={FileText} label="Faturação" active={activeTab === 'billing'} onClick={() => { setActiveTab('billing'); 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={Map} label="Mapa" active={activeTab === 'map'} onClick={() => { setActiveTab('map'); setSidebarOpen(false); }} />
|
||||
|
||||
<div className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2 px-4 mt-6">Espaços Comuns</div>
|
||||
@@ -1012,8 +1033,11 @@
|
||||
activeTab === 'dashboard' ? 'Visão Geral' :
|
||||
activeTab === 'residents' ? 'Condóminos' :
|
||||
activeTab === 'finance' ? 'Gestão Financeira' :
|
||||
activeTab === 'billing' ? 'Faturação e Cobranças' :
|
||||
activeTab === 'maintenance' ? 'Ocorrências e Manutenção' :
|
||||
activeTab === 'messages' ? 'Mensagens e Fórum' :
|
||||
activeTab === 'map' ? 'Mapa do Condomínio' :
|
||||
activeTab === 'all_bookings' ? 'Todas as Reservas' :
|
||||
activeTab === 'gym' ? 'Ginásio' :
|
||||
activeTab === 'hall' ? 'Salão de Festas' :
|
||||
activeTab === 'park' ? 'Parque de Jogos' :
|
||||
@@ -1075,7 +1099,7 @@
|
||||
onClick={() => setActiveTab('profile')}
|
||||
title="Meu Perfil"
|
||||
>
|
||||
AD
|
||||
{userRole === 'admin' ? 'AD' : 'MO'}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -1126,6 +1150,49 @@
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* --- ALL BOOKINGS --- */}
|
||||
{activeTab === 'all_bookings' && (
|
||||
<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="p-6 border-b border-slate-100 dark:border-dark-border flex justify-between items-center">
|
||||
<div>
|
||||
<h3 className="font-bold text-lg text-slate-800 dark:text-white">Histórico de Reservas</h3>
|
||||
<p className="text-sm text-slate-500 dark:text-dark-mute">Lista completa de agendamentos em todos os espaços de lazer</p>
|
||||
</div>
|
||||
<button onClick={() => setActiveTab('map')} className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 flex items-center justify-center gap-2 shadow-sm transition-colors">
|
||||
<Map size={18} /> Nova Reserva
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-auto flex-1 p-4 space-y-3">
|
||||
{bookings.map(booking => (
|
||||
<div key={booking.id} className="flex items-center justify-between p-4 border border-slate-100 dark:border-dark-border rounded-lg hover:bg-slate-50 dark:hover:bg-dark-card transition-colors shadow-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`p-3 rounded-lg ${booking.facility === 'gym' ? 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400' : booking.facility === 'hall' ? 'bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400' : 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400'}`}>
|
||||
{booking.facility === 'gym' ? <Dumbbell size={24} /> : booking.facility === 'hall' ? <PartyPopper size={24} /> : <Trophy size={24} />}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-base text-slate-800 dark:text-slate-200">{booking.facilityName}</p>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">{booking.date} • {booking.time}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-base font-medium text-slate-700 dark:text-slate-300">{booking.resident}</p>
|
||||
<div className="mt-1">
|
||||
<Badge status={booking.status} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{bookings.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<Calendar size={48} className="mx-auto text-slate-300 dark:text-slate-600 mb-4" />
|
||||
<h3 className="text-lg font-bold text-slate-700 dark:text-slate-300">Sem reservas</h3>
|
||||
<p className="text-slate-500 dark:text-slate-400 mt-2">Ainda não existem agendamentos no condomínio.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* --- 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">
|
||||
@@ -1161,6 +1228,7 @@
|
||||
<th className="px-6 py-4">Proprietário</th>
|
||||
<th className="px-6 py-4">Contacto</th>
|
||||
<th className="px-6 py-4">Estado Quotas</th>
|
||||
<th className="px-6 py-4 text-center">Acesso</th>
|
||||
<th className="px-6 py-4 text-right">Em Dívida</th>
|
||||
<th className="px-6 py-4 text-center">Ações</th>
|
||||
</tr>
|
||||
@@ -1177,6 +1245,15 @@
|
||||
</td>
|
||||
<td className="px-6 py-4 text-slate-600 dark:text-dark-mute">{resident.contact}</td>
|
||||
<td className="px-6 py-4"><Badge status={resident.status} /></td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<button
|
||||
onClick={() => handleToggleRole(resident.id)}
|
||||
className={`px-3 py-1 rounded-full text-xs font-bold transition-colors ${resident.role === 'admin' ? 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400 dark:hover:bg-purple-900/50' : 'bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700'}`}
|
||||
title="Mudar Permissões"
|
||||
>
|
||||
{resident.role === 'admin' ? 'Admin' : 'Morador'}
|
||||
</button>
|
||||
</td>
|
||||
<td className={`px-6 py-4 text-right font-medium ${resident.pending > 0 ? 'text-red-600 dark:text-red-400' : 'text-slate-600 dark:text-slate-400'}`}>
|
||||
{Number(resident.pending).toFixed(2)}€
|
||||
</td>
|
||||
@@ -1204,6 +1281,65 @@
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* --- BILLING / COBRANÇAS --- */}
|
||||
{activeTab === 'billing' && userRole === 'admin' && (
|
||||
<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="p-6 border-b border-slate-100 dark:border-dark-border flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<div>
|
||||
<h3 className="font-bold text-lg text-slate-800 dark:text-white">Avisos de Cobrança</h3>
|
||||
<p className="text-sm text-slate-500 dark:text-dark-mute">Emita faturas ou avise condóminos individualmente</p>
|
||||
</div>
|
||||
<button onClick={() => showNotification("Funcionalidade de Emissão Automática em Breve!", "warning")} className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 flex items-center justify-center gap-2 shadow-sm">
|
||||
<FileText size={18} /> Emitir Faturas
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-x-auto flex-1">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="bg-slate-50 dark:bg-dark-bg text-slate-500 dark:text-slate-400 font-medium border-b border-slate-100 dark:border-dark-border">
|
||||
<tr>
|
||||
<th className="px-6 py-4">Fração</th>
|
||||
<th className="px-6 py-4">Condómino</th>
|
||||
<th className="px-6 py-4">Quotas em Atraso</th>
|
||||
<th className="px-6 py-4 text-center">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100 dark:divide-dark-border">
|
||||
{residents.map((resident) => (
|
||||
<tr key={resident.id} className="hover:bg-slate-50 dark:hover:bg-dark-bg transition-colors">
|
||||
<td className="px-6 py-4 font-bold text-slate-700 dark:text-slate-200">{resident.unit}</td>
|
||||
<td className="px-6 py-4 text-slate-800 dark:text-white">{resident.name}</td>
|
||||
<td className={`px-6 py-4 font-medium ${resident.pending > 0 ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-500'}`}>
|
||||
{resident.pending > 0 ? `${Number(resident.pending).toFixed(2)}€` : 'Regularizado'}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<button
|
||||
onClick={() => showNotification(`Aviso de cobrança enviado para ${resident.email}`, 'success')}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-colors shadow-sm cursor-pointer ${resident.pending > 0 ? 'bg-orange-600 text-white hover:bg-orange-700' : 'bg-slate-200 text-slate-400 cursor-not-allowed dark:bg-slate-800 dark:text-slate-600'}`}
|
||||
disabled={resident.pending <= 0}
|
||||
>
|
||||
Notificar
|
||||
</button>
|
||||
<button
|
||||
onClick={() => showNotification(`Fatura instantânea gerada para a fração ${resident.unit}`, 'success')}
|
||||
className="ml-2 px-3 py-1.5 rounded-lg text-xs font-bold transition-colors shadow-sm bg-slate-800 text-white hover:bg-slate-900 dark:bg-slate-700 dark:hover:bg-slate-600"
|
||||
>
|
||||
Faturar na Hora
|
||||
</button>
|
||||
<button
|
||||
onClick={() => showNotification(`Fatura enviada com sucesso para ${resident.email}`, 'success')}
|
||||
className="ml-2 px-3 py-1.5 rounded-lg text-xs font-bold transition-colors shadow-sm bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700"
|
||||
>
|
||||
Enviar por Email
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* --- FINANCES --- */}
|
||||
{/* --- FINANCES --- */}
|
||||
{activeTab === 'finance' && (
|
||||
@@ -1272,7 +1408,7 @@
|
||||
{item.type === 'income' ? '+' : '-'}{Number(item.amount).toFixed(2)}€
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<button className="text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
|
||||
<button onClick={() => showNotification(`Recibo de ${item.category} descarregado.`, 'success')} className="text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors" title="Descarregar Recibo">
|
||||
<FileText size={16} />
|
||||
</button>
|
||||
</td>
|
||||
@@ -1285,6 +1421,118 @@
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* --- MESSAGES --- */}
|
||||
{activeTab === 'messages' && (
|
||||
<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 md:flex-row min-h-[500px]">
|
||||
{/* Contact List */}
|
||||
<div className="w-full md:w-1/3 border-r border-slate-100 dark:border-dark-border flex flex-col h-full">
|
||||
<div className="p-4 border-b border-slate-100 dark:border-dark-border flex justify-between items-center bg-slate-50 dark:bg-dark-bg">
|
||||
<h3 className="font-bold text-slate-800 dark:text-white">Conversas</h3>
|
||||
<button onClick={() => showNotification('Criação de novos grupos em breve!', 'warning')} className="p-2 bg-blue-100 text-blue-600 rounded-lg hover:bg-blue-200 dark:bg-blue-900/40 dark:text-blue-400 transition-colors">
|
||||
<Plus size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-3 border-b border-slate-100 dark:border-dark-border">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={16} />
|
||||
<input type="text" placeholder="Procurar mensagens..." className="w-full pl-9 pr-3 py-2 bg-slate-100 dark:bg-dark-card border-none rounded-lg text-sm focus:ring-2 focus:ring-blue-500 dark:text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<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 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">
|
||||
<Users size={20} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<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>
|
||||
<span className="text-xs text-blue-600 dark:text-blue-400 font-medium whitespace-nowrap">Agora</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 truncate">Administração: Reunião na próxima sexta.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{residents.slice(0, 4).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 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">
|
||||
{res.name.substring(0, 2).toUpperCase()}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<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>
|
||||
<span className="text-xs text-slate-400 whitespace-nowrap">Ontem</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-500 truncate">Tudo bem, tratamos disso!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chat Area */}
|
||||
<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="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">
|
||||
<Users size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-slate-800 dark:text-white">Fórum do Condomínio</h3>
|
||||
<p className="text-xs text-green-500 font-medium">Todos os moradores</p>
|
||||
</div>
|
||||
</div>
|
||||
<button className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200"><Info size={20}/></button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
<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="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-xs text-blue-600 dark:text-blue-400 font-bold mb-1">Administração</p>
|
||||
<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">
|
||||
<div className="bg-blue-600 text-white p-3 rounded-2xl rounded-tr-sm max-w-[80%] shadow-sm">
|
||||
<p className="text-sm">Obrigado pelo aviso!</p>
|
||||
<span className="text-[10px] text-blue-200 block text-right mt-1">09:15</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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-xs text-orange-600 dark:text-orange-400 font-bold mb-1">Maria Pereira (2º Esq)</p>
|
||||
<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 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">
|
||||
<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} />
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
/>
|
||||
<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">
|
||||
<Send size={18} />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* --- MAINTENANCE --- */}
|
||||
{activeTab === 'maintenance' && <MaintenanceView />}
|
||||
|
||||
@@ -1398,6 +1646,6 @@
|
||||
const app = initializeApp(firebaseConfig);
|
||||
const analytics = getAnalytics(app);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user