ajuste tela login

This commit is contained in:
2026-03-18 10:40:57 +00:00
parent 404bcf8637
commit aa64354c06
3 changed files with 397 additions and 72 deletions

View File

@@ -1,3 +1,3 @@
{
"liveServer.settings.port": 5501
"liveServer.settings.port": 5502
}

View File

@@ -1 +1,78 @@
# GestorCondominio
# CondoMaster Pro
![CondoMaster Pro Preview](https://via.placeholder.com/800x400.png?text=CondoMaster+Pro+-+Gestão+de+Condomínios)
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.***

View File

@@ -98,13 +98,13 @@
Dumbbell, PartyPopper, Trophy, Map, Calendar, MapPin, Info
} from 'lucide-react';
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
/>
@@ -321,22 +318,36 @@
const [activeTab, setActiveTab] = useState('dashboard');
const [isSidebarOpen, setSidebarOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [theme, setTheme] = useState('system');
const [theme, setTheme] = useState('system');
const [isAuthenticated, setIsAuthenticated] = useState(() => {
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';
}
}
setIsAuthenticated(true);
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">
<Card title="Saldo Disponível" value={`${balance.toFixed(2)}`} icon={Wallet} trend={balance >= 0 ? 'up' : 'down'} trendValue="Atual" color="bg-blue-500" />
{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>
<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>
{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); }} />
<SidebarItem icon={Wrench} label="Manutenção" active={activeTab === 'maintenance'} onClick={() => { setActiveTab('maintenance'); 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,12 +1033,15 @@
activeTab === 'dashboard' ? 'Visão Geral' :
activeTab === 'residents' ? 'Condóminos' :
activeTab === 'finance' ? 'Gestão Financeira' :
activeTab === 'maintenance' ? 'Ocorrências e Manutenção' :
activeTab === 'map' ? 'Mapa do Condomínio' :
activeTab === 'gym' ? 'Ginásio' :
activeTab === 'hall' ? 'Salão de Festas' :
activeTab === 'park' ? 'Parque de Jogos' :
activeTab === 'profile' ? 'O Meu Perfil' : activeTab
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' :
activeTab === 'profile' ? 'O Meu Perfil' : activeTab
}</h2>
</div>
@@ -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 />}
@@ -1382,22 +1630,22 @@
root.render(<App />);
</script>
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/12.10.0/firebase-app.js";
import { getAnalytics } from "https://www.gstatic.com/firebasejs/12.10.0/firebase-analytics.js";
import { initializeApp } from "https://www.gstatic.com/firebasejs/12.10.0/firebase-app.js";
import { getAnalytics } from "https://www.gstatic.com/firebasejs/12.10.0/firebase-analytics.js";
const firebaseConfig = {
apiKey: "AIzaSyAQHgVDJWM42HfRzvKTxdVW78Qq48vBb2A",
authDomain: "condomaster-pro-ed9af.firebaseapp.com",
projectId: "condomaster-pro-ed9af",
storageBucket: "condomaster-pro-ed9af.firebasestorage.app",
messagingSenderId: "169472241616",
appId: "1:169472241616:web:8e6074dc4b8a6dce9013d5",
measurementId: "G-2BH2VTW6D5"
};
const firebaseConfig = {
apiKey: "AIzaSyAQHgVDJWM42HfRzvKTxdVW78Qq48vBb2A",
authDomain: "condomaster-pro-ed9af.firebaseapp.com",
projectId: "condomaster-pro-ed9af",
storageBucket: "condomaster-pro-ed9af.firebasestorage.app",
messagingSenderId: "169472241616",
appId: "1:169472241616:web:8e6074dc4b8a6dce9013d5",
measurementId: "G-2BH2VTW6D5"
};
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);
</script>
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);
</script>
</body>
</html>