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.***
|
||||||
|
|||||||
388
index.html
388
index.html
@@ -98,13 +98,13 @@
|
|||||||
Dumbbell, PartyPopper, Trophy, Map, Calendar, MapPin, Info
|
Dumbbell, PartyPopper, Trophy, Map, Calendar, MapPin, Info
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
|
||||||
const INITIAL_RESIDENTS = [
|
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: 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 },
|
{ 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 },
|
{ 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 },
|
{ 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 },
|
{ 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 = [
|
const INITIAL_FINANCES = [
|
||||||
@@ -258,12 +258,9 @@
|
|||||||
|
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (email === 'admin@gmail.com' && password === 'admin123') {
|
const success = onLogin(email, password);
|
||||||
onLogin({ role: 'admin', email });
|
if (!success) {
|
||||||
} else if (email === 'moradores@gmail.com' && password === 'moradores123') {
|
setError('Email ou Palavra-passe incorreta');
|
||||||
onLogin({ role: 'resident', email });
|
|
||||||
} else {
|
|
||||||
setError('Credenciais incorretas');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -280,13 +277,13 @@
|
|||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div>
|
<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
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
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"
|
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
|
autoFocus
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -321,22 +318,36 @@
|
|||||||
const [activeTab, setActiveTab] = useState('dashboard');
|
const [activeTab, setActiveTab] = useState('dashboard');
|
||||||
const [isSidebarOpen, setSidebarOpen] = useState(false);
|
const [isSidebarOpen, setSidebarOpen] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [theme, setTheme] = useState('system');
|
const [theme, setTheme] = useState('system');
|
||||||
|
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(() => {
|
const [isAuthenticated, setIsAuthenticated] = useState(() => {
|
||||||
return sessionStorage.getItem('condo_auth') === 'true';
|
return sessionStorage.getItem('condo_auth') === 'true';
|
||||||
});
|
});
|
||||||
const [userRole, setUserRole] = useState(() => {
|
const [userRole, setUserRole] = useState(() => {
|
||||||
return sessionStorage.getItem('condo_role') || 'admin';
|
return sessionStorage.getItem('condo_role') || 'morador';
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleLogin = (userData) => {
|
const handleLogin = (email, password) => {
|
||||||
sessionStorage.setItem('condo_auth', 'true');
|
let role = null;
|
||||||
if (userData && userData.role) {
|
if (email === 'administradores@gmail.com' && password === 'admin123') {
|
||||||
sessionStorage.setItem('condo_role', userData.role);
|
role = 'admin';
|
||||||
setUserRole(userData.role);
|
} 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 = () => {
|
const handleLogout = () => {
|
||||||
@@ -344,6 +355,7 @@
|
|||||||
sessionStorage.removeItem('condo_auth');
|
sessionStorage.removeItem('condo_auth');
|
||||||
sessionStorage.removeItem('condo_role');
|
sessionStorage.removeItem('condo_role');
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
|
setUserRole(null);
|
||||||
setActiveTab('dashboard');
|
setActiveTab('dashboard');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -362,7 +374,7 @@
|
|||||||
|
|
||||||
const notificationRef = useRef(null);
|
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 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 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 };
|
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) => {
|
const handleSaveResident = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (editingItem) {
|
if (editingItem) {
|
||||||
@@ -533,7 +550,11 @@
|
|||||||
const DashboardView = () => (
|
const DashboardView = () => (
|
||||||
<div className="space-y-6 animate-fade-in">
|
<div className="space-y-6 animate-fade-in">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<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="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" />
|
<Card title="Manutenções Ativas" value={activeIssuesCount} icon={Wrench} trend="up" trendValue="Novas" color="bg-orange-500" subtitle="Em resolução" />
|
||||||
</div>
|
</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="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">
|
<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>
|
<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>
|
||||||
<div className="p-4 space-y-3">
|
<div className="p-4 space-y-3">
|
||||||
{bookings.slice(0, 4).map(booking => (
|
{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="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">
|
<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>
|
<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>
|
||||||
<div className="p-4 space-y-3 overflow-y-auto max-h-[350px]">
|
<div className="p-4 space-y-3 overflow-y-auto max-h-[350px]">
|
||||||
{issues.slice(0, 3).map((issue) => (
|
{issues.slice(0, 3).map((issue) => (
|
||||||
@@ -752,7 +773,7 @@
|
|||||||
Prioridade {issue.priority}
|
Prioridade {issue.priority}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{issue.status !== 'Resolvido' && (
|
{userRole === 'admin' && issue.status !== 'Resolvido' && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleResolveIssue(issue.id)}
|
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"
|
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="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="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">
|
<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>
|
</div>
|
||||||
<h3 className="font-bold text-slate-800 dark:text-white">Admin Condomínio</h3>
|
<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">Administrador Geral</p>
|
<p className="text-xs text-slate-500 dark:text-dark-mute">{userRole === 'admin' ? 'Administrador Geral' : 'Residente'}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -797,12 +818,14 @@
|
|||||||
>
|
>
|
||||||
<LogOut size={18} className="rotate-90" /> Segurança
|
<LogOut size={18} className="rotate-90" /> Segurança
|
||||||
</button>
|
</button>
|
||||||
<button
|
{userRole === 'admin' && (
|
||||||
onClick={() => setActiveSection('permissions')}
|
<button
|
||||||
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'}`}
|
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>
|
<CheckCircle size={18} /> Permissões
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveSection('settings')}
|
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'}`}
|
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>
|
</div>
|
||||||
<InputGroup label="Morada (Sede)" value="Rua das Flores, nº 123, Escritório 2B" />
|
<InputGroup label="Morada (Sede)" value="Rua das Flores, nº 123, Escritório 2B" />
|
||||||
<div className="flex justify-end mt-6">
|
<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
|
Guardar Alterações
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -842,8 +865,8 @@
|
|||||||
<AlertCircle className="text-orange-600 dark:text-orange-400 flex-shrink-0 mt-0.5" size={20} />
|
<AlertCircle className="text-orange-600 dark:text-orange-400 flex-shrink-0 mt-0.5" size={20} />
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-bold text-orange-800 dark:text-orange-300 text-sm">Autenticação de Dois Fatores (2FA)</h4>
|
<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>
|
<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 className="text-orange-900 dark:text-orange-200 text-xs font-bold underline mt-2">Ativar Agora</button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -854,7 +877,7 @@
|
|||||||
<InputGroup label="Confirmar Nova Palavra-passe" type="password" placeholder="Confirmar" />
|
<InputGroup label="Confirmar Nova Palavra-passe" type="password" placeholder="Confirmar" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end mt-6">
|
<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
|
Atualizar Segurança
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -976,13 +999,11 @@
|
|||||||
<div className="flex-1 px-4 py-2 space-y-1 overflow-y-auto">
|
<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>
|
<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); }} />
|
<SidebarItem icon={Building2} label="Dashboard" active={activeTab === 'dashboard'} onClick={() => { setActiveTab('dashboard'); setSidebarOpen(false); }} />
|
||||||
{userRole === 'admin' && (
|
{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); }} />}
|
||||||
<SidebarItem icon={Users} label="Condóminos" active={activeTab === 'residents'} onClick={() => { setActiveTab('residents'); setSidebarOpen(false); }} />
|
{userRole === 'admin' && <SidebarItem icon={FileText} label="Faturação" active={activeTab === 'billing'} onClick={() => { setActiveTab('billing'); 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); }} />
|
||||||
<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); }} />
|
<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>
|
<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 === 'dashboard' ? 'Visão Geral' :
|
||||||
activeTab === 'residents' ? 'Condóminos' :
|
activeTab === 'residents' ? 'Condóminos' :
|
||||||
activeTab === 'finance' ? 'Gestão Financeira' :
|
activeTab === 'finance' ? 'Gestão Financeira' :
|
||||||
activeTab === 'maintenance' ? 'Ocorrências e Manutenção' :
|
activeTab === 'billing' ? 'Faturação e Cobranças' :
|
||||||
activeTab === 'map' ? 'Mapa do Condomínio' :
|
activeTab === 'maintenance' ? 'Ocorrências e Manutenção' :
|
||||||
activeTab === 'gym' ? 'Ginásio' :
|
activeTab === 'messages' ? 'Mensagens e Fórum' :
|
||||||
activeTab === 'hall' ? 'Salão de Festas' :
|
activeTab === 'map' ? 'Mapa do Condomínio' :
|
||||||
activeTab === 'park' ? 'Parque de Jogos' :
|
activeTab === 'all_bookings' ? 'Todas as Reservas' :
|
||||||
activeTab === 'profile' ? 'O Meu Perfil' : activeTab
|
activeTab === 'gym' ? 'Ginásio' :
|
||||||
|
activeTab === 'hall' ? 'Salão de Festas' :
|
||||||
|
activeTab === 'park' ? 'Parque de Jogos' :
|
||||||
|
activeTab === 'profile' ? 'O Meu Perfil' : activeTab
|
||||||
}</h2>
|
}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1075,7 +1099,7 @@
|
|||||||
onClick={() => setActiveTab('profile')}
|
onClick={() => setActiveTab('profile')}
|
||||||
title="Meu Perfil"
|
title="Meu Perfil"
|
||||||
>
|
>
|
||||||
AD
|
{userRole === 'admin' ? 'AD' : 'MO'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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 --- */}
|
{/* --- 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">
|
||||||
@@ -1161,6 +1228,7 @@
|
|||||||
<th className="px-6 py-4">Proprietário</th>
|
<th className="px-6 py-4">Proprietário</th>
|
||||||
<th className="px-6 py-4">Contacto</th>
|
<th className="px-6 py-4">Contacto</th>
|
||||||
<th className="px-6 py-4">Estado Quotas</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-right">Em Dívida</th>
|
||||||
<th className="px-6 py-4 text-center">Ações</th>
|
<th className="px-6 py-4 text-center">Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1177,6 +1245,15 @@
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 text-slate-600 dark:text-dark-mute">{resident.contact}</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"><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'}`}>
|
<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)}€
|
{Number(resident.pending).toFixed(2)}€
|
||||||
</td>
|
</td>
|
||||||
@@ -1204,6 +1281,65 @@
|
|||||||
</div>
|
</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 --- */}
|
||||||
{/* --- FINANCES --- */}
|
{/* --- FINANCES --- */}
|
||||||
{activeTab === 'finance' && (
|
{activeTab === 'finance' && (
|
||||||
@@ -1272,7 +1408,7 @@
|
|||||||
{item.type === 'income' ? '+' : '-'}{Number(item.amount).toFixed(2)}€
|
{item.type === 'income' ? '+' : '-'}{Number(item.amount).toFixed(2)}€
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 text-center">
|
<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} />
|
<FileText size={16} />
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
@@ -1285,6 +1421,118 @@
|
|||||||
</div>
|
</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 --- */}
|
{/* --- MAINTENANCE --- */}
|
||||||
{activeTab === 'maintenance' && <MaintenanceView />}
|
{activeTab === 'maintenance' && <MaintenanceView />}
|
||||||
|
|
||||||
@@ -1382,22 +1630,22 @@
|
|||||||
root.render(<App />);
|
root.render(<App />);
|
||||||
</script>
|
</script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { initializeApp } from "https://www.gstatic.com/firebasejs/12.10.0/firebase-app.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";
|
import { getAnalytics } from "https://www.gstatic.com/firebasejs/12.10.0/firebase-analytics.js";
|
||||||
|
|
||||||
const firebaseConfig = {
|
const firebaseConfig = {
|
||||||
apiKey: "AIzaSyAQHgVDJWM42HfRzvKTxdVW78Qq48vBb2A",
|
apiKey: "AIzaSyAQHgVDJWM42HfRzvKTxdVW78Qq48vBb2A",
|
||||||
authDomain: "condomaster-pro-ed9af.firebaseapp.com",
|
authDomain: "condomaster-pro-ed9af.firebaseapp.com",
|
||||||
projectId: "condomaster-pro-ed9af",
|
projectId: "condomaster-pro-ed9af",
|
||||||
storageBucket: "condomaster-pro-ed9af.firebasestorage.app",
|
storageBucket: "condomaster-pro-ed9af.firebasestorage.app",
|
||||||
messagingSenderId: "169472241616",
|
messagingSenderId: "169472241616",
|
||||||
appId: "1:169472241616:web:8e6074dc4b8a6dce9013d5",
|
appId: "1:169472241616:web:8e6074dc4b8a6dce9013d5",
|
||||||
measurementId: "G-2BH2VTW6D5"
|
measurementId: "G-2BH2VTW6D5"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const app = initializeApp(firebaseConfig);
|
||||||
|
const analytics = getAnalytics(app);
|
||||||
|
</script>
|
||||||
|
|
||||||
const app = initializeApp(firebaseConfig);
|
|
||||||
const analytics = getAnalytics(app);
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user