import React, { useState, useEffect, useRef } from 'react'; import { createRoot } from 'react-dom/client'; import { Building2, Users, Wallet, Wrench, Bell, Search, Plus, Menu, X, TrendingUp, TrendingDown, CheckCircle, AlertCircle, Clock, LogOut, Edit2, Trash2, Save, Filter, MoreVertical, FileText, Dumbbell, PartyPopper, Trophy, Map, Calendar, MapPin, Info, MessageCircle, Paperclip, Send, Store, HeartPulse, Waves, ShoppingCart, Navigation, Car } from 'lucide-react'; import { app } from './firebase.js'; import { getAuth, signInWithEmailAndPassword, createUserWithEmailAndPassword } from 'https://www.gstatic.com/firebasejs/12.1.0/firebase-auth.js'; import { getDatabase, ref, push, set, onValue, remove, update } from 'https://www.gstatic.com/firebasejs/12.1.0/firebase-database.js'; class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false, error: null, errorInfo: null }; } static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, errorInfo) { this.setState({ error, errorInfo }); console.error("ErrorBoundary caught an error:", error, errorInfo); } render() { if (this.state.hasError) { return (

Algo correu mal (Erro na Aplicação)

                                {this.state.error && this.state.error.toString()}
                                
{this.state.errorInfo && this.state.errorInfo.componentStack}
); } return this.props.children; } } const auth = getAuth(app); const db = getDatabase(app); const INITIAL_RESIDENTS = [ { 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 = [ { id: 1, type: 'income', category: 'Quotas Mensais', date: '2023-10-01', amount: 2250.00, desc: 'Pagamento de quotas Outubro' }, { id: 2, type: 'expense', category: 'Limpeza', date: '2023-10-02', amount: 450.00, desc: 'Serviço de Limpeza Semanal' }, { id: 3, type: 'expense', category: 'Elevadores', date: '2023-10-05', amount: 120.00, desc: 'Manutenção Mensal' }, { id: 4, type: 'income', category: 'Aluguer Salão', date: '2023-10-10', amount: 50.00, desc: 'Reserva 2º Dto' }, { id: 5, type: 'expense', category: 'Jardinagem', date: '2023-10-12', amount: 85.00, desc: 'Poda de árvores' }, ]; const INITIAL_ISSUES = [ { id: 1, title: 'Lâmpada fundida no Hall', location: 'R/C', status: 'Novo', priority: 'Baixa', date: '2023-10-15' }, { id: 2, title: 'Porta da garagem não fecha', location: 'Garagem -1', status: 'Em Progresso', priority: 'Alta', date: '2023-10-14' }, { id: 3, title: 'Infiltração no teto', location: '3º Dto', status: 'Resolvido', priority: 'Média', date: '2023-10-10' }, ]; const INITIAL_BOOKINGS = [ { id: 1, facility: 'hall', facilityName: 'Salão de Festas', date: '2023-10-25', time: '14:00 - 20:00', resident: 'Ana Silva', status: 'Confirmado', cost: 50 }, { id: 2, facility: 'gym', facilityName: 'Ginásio', date: '2023-10-20', time: '09:00 - 10:00', resident: 'Carlos Santos', status: 'Confirmado', cost: 0 }, { id: 3, facility: 'park', facilityName: 'Parque de Jogos', date: '2023-10-22', time: '18:00 - 19:00', resident: 'Sofia Costa', status: 'Pendente', cost: 10 }, ]; const INITIAL_NOTIFICATIONS = [ { id: 1, message: 'Nova reserva: Salão de Festas (25 Out)', time: 'Há 1 hora', type: 'info', read: false }, { id: 2, message: 'Nova quota paga: Maria Pereira', time: 'Há 2 horas', type: 'success', read: false }, { id: 3, message: 'Manutenção urgente reportada', time: 'Há 5 horas', type: 'warning', read: false }, ]; // --- VALIDAÇÕES OFICIAIS --- function validarNIF(nif) { nif = String(nif).replace(/\s+/g, ''); return /^\d{9}$/.test(nif); } function validarDocumento(doc) { let docStr = doc.replace(/[\s-]/g, '').toUpperCase(); // Muitos utilizadores inserem apenas os 8 dígitos do NIC, o que não tem check-digit na própria string if (/^\d{8}$/.test(docStr)) { return true; } if (/^\d{9}$/.test(docStr)) { let checkDigitValue = parseInt(docStr.charAt(docStr.length - 1), 10); let soma = 0; for (let i = 0; i < docStr.length - 1; i++) { soma += parseInt(docStr.charAt(i), 10) * (docStr.length - i); } let resto = soma % 11; let expectedDigit = (resto === 0 || resto === 1) ? 0 : (11 - resto); return expectedDigit === checkDigitValue; } if (docStr.length === 12) { let sum = 0; let isSecond = false; for (let i = docStr.length - 1; i >= 0; i--) { let charCode = docStr.charCodeAt(i); let val = 0; if (charCode >= 48 && charCode <= 57) val = charCode - 48; else if (charCode >= 65 && charCode <= 90) val = charCode - 55; else return false; if (isSecond) { val *= 2; if (val >= 36) val -= 36; } sum += val; isSecond = !isSecond; } return (sum % 36) === 0; } return false; } const Modal = ({ isOpen, onClose, title, children }) => { if (!isOpen) return null; return (

{title}

{children}
); }; const InputGroup = ({ label, name, type = 'text', value, onChange, placeholder, required = false, options = null, disabled = false }) => (
{options ? ( ) : ( )}
); const SidebarItem = ({ icon: Icon, label, active, onClick }) => ( ); const Card = ({ title, value, icon: Icon, trend, trendValue, color, subtitle }) => (

{title}

{value}

{trend === 'up' ? ( {trendValue} ) : trend === 'down' ? ( {trendValue} ) : ( )}
{subtitle || 'vs. mês passado'}
); const Badge = ({ status }) => { const styles = { 'Pago': 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800', 'Em dia': 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800', 'Resolvido': 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800', 'Receita': 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800', 'Confirmado': 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800', 'Pendente': 'bg-yellow-100 text-yellow-700 border-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400 dark:border-yellow-800', 'Em Validação': 'bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-900/30 dark:text-orange-400 dark:border-orange-800', 'Em Progresso': 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800', 'Média': 'bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-900/30 dark:text-orange-400 dark:border-orange-800', 'Atrasado': 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800', 'Despesa': 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800', 'Alta': 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800', 'Novo': 'bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-900/30 dark:text-purple-400 dark:border-purple-800', 'Baixa': 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800', }; return ( {status} ); }; const LoginView = ({ onLogin }) => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); const handleSubmit = async (e) => { e.preventDefault(); if (isLoading) return; setError(''); setIsLoading(true); const success = await onLogin(email, password); setIsLoading(false); if (!success) { setError('Email ou Palavra-passe incorreta'); setTimeout(() => { setError(''); }, 5000); } }; return (

MyCondominium

Portal de Gestão

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 email" autoFocus required />
setPassword(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="Senha de acesso" required />
{error && (
{error}
)}
); }; function App() { const [activeTab, setActiveTab] = useState('dashboard'); const [isSidebarOpen, setSidebarOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [theme, setTheme] = useState('system'); const [isAuthenticated, setIsAuthenticated] = useState(() => { return sessionStorage.getItem('condo_auth') === 'true'; }); const [userRole, setUserRole] = useState(() => { return sessionStorage.getItem('condo_role') || 'morador'; }); const [currentUserName, setCurrentUserName] = useState(() => { return sessionStorage.getItem('condo_user_name') || 'Utilizador'; }); const [currentUserId, setCurrentUserId] = useState(() => { return sessionStorage.getItem('condo_user_id') || '0'; }); const [userStatus, setUserStatus] = useState(() => { return sessionStorage.getItem('condo_user_status') || 'aprovado'; }); const handleLogin = async (email, password) => { try { const userCredential = await signInWithEmailAndPassword(auth, email, password); let role = 'morador'; let userName = 'Utilizador'; let userId = userCredential.user.uid; let status = 'aprovado'; if (email.toLowerCase().includes('admin')) { role = 'admin'; userName = 'Administração'; } else { const residentUser = residents.find(r => r.id === userId || (r.email && r.email.toLowerCase() === email.toLowerCase())); if (residentUser) { role = residentUser.role || 'morador'; userName = residentUser.name + (residentUser.unit && residentUser.unit !== 'Pendente' ? ` (${residentUser.unit})` : ''); userId = residentUser.id || userId; status = residentUser.status || 'aprovado'; } else { status = 'pendente'; // Fallback if missing } } sessionStorage.setItem('condo_auth', 'true'); sessionStorage.setItem('condo_role', role); sessionStorage.setItem('condo_user_name', userName); sessionStorage.setItem('condo_user_id', userId); sessionStorage.setItem('condo_user_status', status); setIsAuthenticated(true); setUserRole(role); setCurrentUserName(userName); setCurrentUserId(userId); setUserStatus(status); return true; } catch (error) { console.log("Firebase Auth falhou, a tentar conta local...", error); let role = null; let userName = 'Utilizador'; let userId = 'local_' + Date.now(); let status = 'aprovado'; if (email === 'administradores@gmail.com' && password === 'admin123') { role = 'admin'; userName = 'Administração'; userId = 'admin_001'; } else { const residentUser = residents.find(r => r.email && r.email.toLowerCase() === email.toLowerCase()); if (residentUser && (password === residentUser.password || (!residentUser.password && password === residentUser.contact) || password === '1234')) { role = residentUser.role || 'morador'; userName = residentUser.name + (residentUser.unit && residentUser.unit !== 'Pendente' ? ` (${residentUser.unit})` : ''); userId = residentUser.id || userId; status = residentUser.status || 'aprovado'; } } if (role) { sessionStorage.setItem('condo_auth', 'true'); sessionStorage.setItem('condo_role', role); sessionStorage.setItem('condo_user_name', userName); sessionStorage.setItem('condo_user_id', userId); sessionStorage.setItem('condo_user_status', status); setIsAuthenticated(true); setUserRole(role); setCurrentUserName(userName); setCurrentUserId(userId); setUserStatus(status); return true; } return false; } }; const handleLogout = () => { if (window.confirm('Tem a certeza que deseja terminar sessão?')) { sessionStorage.removeItem('condo_auth'); sessionStorage.removeItem('condo_role'); sessionStorage.removeItem('condo_user_name'); sessionStorage.removeItem('condo_user_id'); sessionStorage.removeItem('condo_user_status'); setIsAuthenticated(false); setUserRole(null); setCurrentUserName('Utilizador'); setCurrentUserId('0'); setUserStatus('aprovado'); setActiveTab('dashboard'); } }; const [residents, setResidents] = useState([]); const [finances, setFinances] = useState([]); const [issues, setIssues] = useState([]); const [bookings, setBookings] = useState([]); const [invoices, setInvoices] = useState([]); const [faturas, setFaturas] = useState([]); const [messages, setMessages] = useState([]); const [newMessageText, setNewMessageText] = useState(''); const [activeChat, setActiveChat] = useState({ type: 'global', id: 'global', name: 'Fórum do Condomínio' }); const [chatGroups, setChatGroups] = useState([]); const [isCreateGroupModalOpen, setIsCreateGroupModalOpen] = useState(false); const [newGroupName, setNewGroupName] = useState(''); const [newGroupMembers, setNewGroupMembers] = useState([]); useEffect(() => { const loadData = (path, setter, sortFunc = null) => { return onValue(ref(db, path), (snapshot) => { const data = snapshot.val(); if (data) { let parsed = Object.entries(data).map(([id, val]) => { if (path === 'faturas' && val.status === 'Em Validação') { return { id, ...val, status: 'Pago' }; } return { id, ...val }; }); if (userRole !== 'admin' && (path === 'manutencao' || path === 'reservas')) { parsed = parsed.filter(item => item.moradorId === currentUserId); } if (sortFunc) parsed = parsed.sort(sortFunc); setter(parsed); } else { setter([]); } }, (error) => console.error(`Erro ao carregar ${path}:`, error)); }; const unsubResidents = loadData('condominos', setResidents); const unsubFinances = loadData('financas', setFinances, (a,b) => new Date(b.date) - new Date(a.date)); const unsubIssues = loadData('manutencao', setIssues, (a,b) => new Date(b.date) - new Date(a.date)); const unsubBookings = loadData('reservas', setBookings, (a,b) => new Date(a.date) - new Date(b.date)); const unsubInvoices = loadData('faturacao', setInvoices, (a,b) => new Date(b.date) - new Date(a.date)); const unsubFaturas = loadData('faturas', setFaturas, (a,b) => new Date(b.dataVencimento) - new Date(a.dataVencimento)); const unsubGroups = loadData('grupos_chat', setChatGroups); return () => { unsubResidents(); unsubFinances(); unsubIssues(); unsubBookings(); unsubInvoices(); unsubFaturas(); unsubGroups(); }; }, []); useEffect(() => { if (!isAuthenticated || !currentUserId) { setNotificationsList([]); return; } const targetFolder = userRole === 'admin' ? 'admin' : currentUserId; const path = `notificacoes/${targetFolder}`; const unsub = onValue(ref(db, path), (snapshot) => { const data = snapshot.val(); if (data) { let parsed = Object.entries(data).map(([id, val]) => ({ id, ...val })); parsed = parsed.sort((a,b) => b.timestamp - a.timestamp); setNotificationsList(parsed); } else { setNotificationsList([]); } }, (error) => console.error(`Erro ao carregar notificações:`, error)); return () => unsub(); }, [isAuthenticated, currentUserId, userRole]); useEffect(() => { let path = 'mural_mensagens'; if (activeChat.type === 'private') { path = `mensagens_privadas/${[currentUserId, activeChat.id].sort().join('_')}`; } else if (activeChat.type === 'group') { path = `mensagens_grupo/${activeChat.id}`; } const unsub = onValue(ref(db, path), (snapshot) => { const data = snapshot.val(); if (data) { let parsed = Object.entries(data).map(([id, val]) => ({ id, ...val })); parsed = parsed.sort((a,b) => a.timestamp - b.timestamp); setMessages(parsed); } else { setMessages([]); } }, (error) => console.error(`Erro ao carregar mensagens de ${path}:`, error)); return () => unsub(); }, [activeChat, currentUserId]); useEffect(() => { if (userRole === 'admin' && residents.length > 0 && faturas.length > 0) { let hasUpdates = false; const updates = {}; residents.forEach((resident) => { const residentFaturas = faturas.filter(f => f.moradorId === resident.id && f.status !== 'Pago'); const actualPending = residentFaturas.reduce((acc, f) => acc + Number(f.valor), 0); const actualStatus = actualPending > 0 ? 'Pendente' : 'Pago'; if (Number(resident.pending) !== actualPending || resident.status !== actualStatus) { updates[`condominos/${resident.id}/pending`] = actualPending; updates[`condominos/${resident.id}/status`] = actualStatus; hasUpdates = true; } }); if (hasUpdates) { update(ref(db), updates).catch(err => console.error("Erro na sincronização:", err)); } } }, [faturas, residents, userRole]); const [notificationsList, setNotificationsList] = useState([]); const [isNotificationsOpen, setNotificationsOpen] = useState(false); const [activeModal, setActiveModal] = useState(null); const [editingItem, setEditingItem] = useState(null); const [notification, setNotification] = useState(null); const notificationRef = useRef(null); 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 }; const initialFaturaForm = { moradorId: '', categoria: '', valor: '', dataVencimento: new Date().toISOString().split('T')[0] }; const [formData, setFormData] = useState({}); useEffect(() => { const root = window.document.documentElement; root.classList.remove('dark'); if (theme === 'dark') { root.classList.add('dark'); } else if (theme === 'system') { if (window.matchMedia('(prefers-color-scheme: dark)').matches) { root.classList.add('dark'); } } }, [theme]); useEffect(() => { if (notification) { const timer = setTimeout(() => setNotification(null), 3000); return () => clearTimeout(timer); } }, [notification]); useEffect(() => { function handleClickOutside(event) { if (notificationRef.current && !notificationRef.current.contains(event.target)) { setNotificationsOpen(false); } } document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [notificationRef]); const toggleSidebar = () => setSidebarOpen(!isSidebarOpen); const totalIncome = finances.filter(f => f.type === 'income').reduce((acc, curr) => acc + Number(curr.amount), 0); const totalExpense = finances.filter(f => f.type === 'expense').reduce((acc, curr) => acc + Number(curr.amount), 0); const balance = totalIncome - totalExpense; const activeIssuesCount = issues.filter(i => i.status !== 'Resolvido').length; const unreadNotifications = notificationsList.filter(n => !n.read).length; const filteredResidents = residents.filter(r => r.name.toLowerCase().includes(searchQuery.toLowerCase()) || r.unit.toLowerCase().includes(searchQuery.toLowerCase()) ); const sendSystemNotification = async (message, type = 'info', targetUserId = 'admin') => { const newNotif = { timestamp: Date.now(), message, time: 'Agora', type, read: false }; if (targetUserId === 'todos') { const promises = residents.map(r => push(ref(db, `notificacoes/${r.id}`), newNotif)); promises.push(push(ref(db, `notificacoes/admin`), newNotif)); await Promise.all(promises); } else { await push(ref(db, `notificacoes/${targetUserId}`), newNotif); } }; const handleMarkAsRead = async (notifId) => { const targetFolder = userRole === 'admin' ? 'admin' : currentUserId; const notifRef = ref(db, `notificacoes/${targetFolder}/${notifId}`); await update(notifRef, { read: true }); }; const showNotification = (message, type = 'success') => { setNotification({ message, type }); }; const handleClearNotifications = async () => { const targetFolder = userRole === 'admin' ? 'admin' : currentUserId; await set(ref(db, `notificacoes/${targetFolder}`), null); setNotificationsOpen(false); }; const handleOpenModal = (type, item = null, defaultFacility = null) => { setEditingItem(item); setActiveModal(type); if (type === 'resident') { setFormData(item || initialResidentForm); } else if (type === 'finance') { setFormData(initialFinanceForm); } else if (type === 'issue') { setFormData(initialIssueForm); } else if (type === 'emitir_fatura') { setFormData(initialFaturaForm); } else if (type === 'booking') { const baseForm = { ...initialBookingForm }; if (defaultFacility) baseForm.facility = defaultFacility; // Preenche sempre o nome do utilizador logado por defeito baseForm.resident = currentUserName; setFormData(baseForm); } }; const handleCloseModal = () => { setActiveModal(null); setEditingItem(null); setFormData({}); }; const handleInputChange = (e) => { const { name, value } = e.target; setFormData(prev => { const newData = { ...prev, [name]: value }; if (name === 'facility' || name === 'time') { let cost = 0; if (newData.facility === 'hall') cost = 50; if (newData.facility === 'park') cost = 10; newData.cost = cost; } return newData; }); }; const handleToggleRole = async (id) => { try { const resident = residents.find(r => r.id === id); if (resident) { const newRole = resident.role === 'admin' ? 'morador' : 'admin'; const residentRef = ref(db, `condominos/${id}`); await set(residentRef, { ...resident, role: newRole }); showNotification('Permissões de utilizador atualizadas', 'success'); } } catch (error) { console.error("Erro ao atualizar permissão:", error); showNotification("Erro ao atualizar permissão.", "error"); } }; const handleSaveResident = async (e) => { e.preventDefault(); try { if (editingItem) { const residentRef = ref(db, `condominos/${editingItem.id}`); const updatedData = { ...editingItem, unit: formData.unit || '', name: formData.name || '', contact: formData.contact || '', email: formData.email || '', status: formData.status || 'Pago', pending: Number(formData.pending) || 0, role: formData.role || 'morador' }; if (formData.password) { updatedData.password = formData.password; } await set(residentRef, updatedData); showNotification(`Condómino ${formData.name} atualizado`); } else { const residentsListRef = ref(db, 'condominos'); const newResidentRef = push(residentsListRef); await set(newResidentRef, { unit: formData.unit || '', name: formData.name || '', contact: formData.contact || '', email: formData.email || '', password: formData.password || '1234', status: formData.status || 'Pago', pending: Number(formData.pending) || 0, role: formData.role || 'morador' }); showNotification(`Novo condómino ${formData.name} adicionado`); } handleCloseModal(); } catch (error) { console.error("Erro ao guardar no Firebase:", error); showNotification("Erro ao guardar os dados.", "error"); } }; const handleDeleteResident = async (id) => { if (window.confirm('Tem a certeza que deseja eliminar este condómino?')) { try { const residentRef = ref(db, `condominos/${id}`); await remove(residentRef); showNotification('Condómino removido', 'error'); } catch (error) { console.error("Erro ao eliminar no Firebase:", error); showNotification("Erro ao eliminar.", "error"); } } }; const handleSaveFinance = async (e) => { e.preventDefault(); if (!formData.amount || !formData.category || !formData.date) { showNotification("Preencha todos os campos obrigatórios.", "error"); return; } try { const amount = Number(formData.amount); const newFinanceRef = push(ref(db, 'financas')); await set(newFinanceRef, { ...formData, amount }); if (formData.type === 'expense') { sendSystemNotification(`Nova despesa registada: ${formData.category} - ${amount.toFixed(2)}€`, 'warning', 'admin'); } else { sendSystemNotification(`Nova receita registada: ${formData.category} - ${amount.toFixed(2)}€`, 'success', 'admin'); } showNotification(`Movimento de ${amount}€ registado`); handleCloseModal(); } catch (error) { console.error("Erro ao guardar finanças:", error); showNotification("Erro ao guardar movimento.", "error"); } }; const handleSaveIssue = async (e) => { e.preventDefault(); if (!formData.title || !formData.location) { showNotification("Preencha todos os campos obrigatórios.", "error"); return; } try { const newIssueRef = push(ref(db, 'manutencao')); await set(newIssueRef, { ...formData, moradorId: currentUserId }); sendSystemNotification(`Nova ocorrência reportada: ${formData.title} (${formData.location})`, 'warning', 'admin'); if (userRole !== 'admin') { sendSystemNotification(`A sua ocorrência "${formData.title}" foi reportada com sucesso.`, 'info', currentUserId); } showNotification('Nova ocorrência reportada', 'warning'); handleCloseModal(); } catch (error) { console.error("Erro ao reportar ocorrência:", error); showNotification("Erro ao reportar ocorrência.", "error"); } }; const handleSaveFatura = async (e) => { e.preventDefault(); if (!formData.moradorId || !formData.categoria || !formData.valor || !formData.dataVencimento) { showNotification("Preencha todos os campos obrigatórios.", "error"); return; } try { const morador = residents.find(r => r.id === formData.moradorId); if (!morador) return; const valor = Number(formData.valor); const newFaturaRef = push(ref(db, 'faturas')); await set(newFaturaRef, { moradorId: morador.id, nomeMorador: morador.name, fracao: morador.unit, categoria: formData.categoria, valor: valor, dataVencimento: formData.dataVencimento, status: 'Pendente', dataEmissao: new Date().toISOString().split('T')[0] }); const newPending = (Number(morador.pending) || 0) + valor; await update(ref(db, `condominos/${morador.id}`), { pending: newPending, status: 'Pendente' }); sendSystemNotification(`Foi emitida uma nova fatura no valor de ${valor.toFixed(2)}€ (Categoria: ${formData.categoria})`, 'warning', morador.id); sendSystemNotification(`Fatura de ${valor.toFixed(2)}€ emitida para ${morador.name} (${morador.unit})`, 'info', 'admin'); showNotification(`Fatura de ${valor.toFixed(2)}€ emitida para ${morador.name}`); handleCloseModal(); } catch (error) { console.error("Erro ao emitir fatura:", error); showNotification("Erro ao emitir fatura.", "error"); } }; const handlePayFatura = async (fatura) => { try { await set(ref(db, `faturas/${fatura.id}/status`), 'Pago'); const morador = residents.find(r => r.id === fatura.moradorId); if (morador) { let newPending = (Number(morador.pending) || 0) - Number(fatura.valor); if (newPending <= 0.01) newPending = 0; await update(ref(db, `condominos/${morador.id}`), { pending: newPending, status: newPending === 0 ? 'Pago' : 'Pendente' }); } sendSystemNotification(`O pagamento da sua fatura de ${fatura.categoria} foi concluído!`, 'success', fatura.moradorId); sendSystemNotification(`Pagamento registado para a fatura de ${fatura.categoria} da fração ${morador?.unit || fatura.fracao}.`, 'success', 'admin'); showNotification("Pagamento efetuado com sucesso!", "success"); } catch (error) { console.error("Erro ao pagar fatura:", error); showNotification("Erro ao processar pagamento.", "error"); } }; const handleApproveFatura = async (fatura) => { try { await set(ref(db, `faturas/${fatura.id}/status`), 'Pago'); const morador = residents.find(r => r.id === fatura.moradorId); if (morador) { let newPending = (Number(morador.pending) || 0) - Number(fatura.valor); if (newPending <= 0.01) newPending = 0; await update(ref(db, `condominos/${morador.id}`), { pending: newPending, status: newPending === 0 ? 'Pago' : 'Pendente' }); } sendSystemNotification(`O seu pagamento da fatura de ${fatura.categoria} foi aprovado!`, 'success', fatura.moradorId); sendSystemNotification(`Pagamento aprovado para a fatura de ${fatura.categoria} da fração ${morador?.unit || fatura.fracao}.`, 'success', 'admin'); showNotification("Pagamento aprovado com sucesso!", "success"); } catch (error) { console.error("Erro ao aprovar fatura:", error); showNotification("Erro ao processar aprovação.", "error"); } }; const handleResolveIssue = async (id) => { try { const issue = issues.find(i => i.id === id); if (issue) { await set(ref(db, `manutencao/${id}`), { ...issue, status: 'Resolvido' }); sendSystemNotification(`A manutenção "${issue.title}" foi concluída com sucesso.`, 'success', 'todos'); showNotification('Ocorrência resolvida com sucesso'); } } catch (error) { console.error("Erro ao resolver ocorrência:", error); showNotification("Erro ao resolver ocorrência.", "error"); } }; const handleSaveBooking = async (e) => { e.preventDefault(); if (!formData.resident || !formData.date || !formData.time) { showNotification("Preencha todos os campos obrigatórios.", "error"); return; } try { const facilityNames = { 'gym': 'Ginásio', 'hall': 'Salão de Festas', 'park': 'Parque de Jogos' }; const bookingData = { ...formData, facilityName: facilityNames[formData.facility], status: 'Confirmado', moradorId: currentUserId }; const newBookingRef = push(ref(db, 'reservas')); await set(newBookingRef, bookingData); if (bookingData.cost > 0) { const newIncomeRef = push(ref(db, 'financas')); await set(newIncomeRef, { type: 'income', category: `Reserva: ${bookingData.facilityName}`, date: bookingData.date, amount: bookingData.cost, desc: `Reserva por ${bookingData.resident}` }); } sendSystemNotification(`Nova reserva: ${bookingData.facilityName} a ${bookingData.date}`, 'info', 'admin'); if (userRole !== 'admin') { sendSystemNotification(`A sua reserva para ${bookingData.facilityName} foi confirmada.`, 'success', currentUserId); } showNotification(`Reserva confirmada para ${bookingData.facilityName}`); handleCloseModal(); } catch (error) { console.error("Erro ao criar reserva:", error); showNotification("Erro ao criar reserva.", "error"); } }; const handleGenerateInvoice = async (resident) => { if (resident.pending <= 0) { showNotification(`Não há dívidas para a fração ${resident.unit}`, 'warning'); return; } try { const newInvoiceRef = push(ref(db, 'faturacao')); await set(newInvoiceRef, { residentId: resident.id, unit: resident.unit, name: resident.name, amount: Number(resident.pending), date: new Date().toISOString().split('T')[0], status: 'Emitida' }); sendSystemNotification(`Foi emitida uma nova fatura instantânea no valor de ${Number(resident.pending).toFixed(2)}€`, 'warning', resident.id); sendSystemNotification(`Fatura instantânea gerada para a fração ${resident.unit} no valor de ${Number(resident.pending).toFixed(2)}€`, 'info', 'admin'); showNotification(`Fatura instantânea gerada para a fração ${resident.unit}`, 'success'); } catch (error) { console.error("Erro ao faturar:", error); showNotification("Erro ao gerar fatura.", "error"); } }; const DashboardView = () => (
{userRole === 'admin' ? ( = 0 ? 'up' : 'down'} trendValue="Atual" color="bg-blue-500" /> ) : ( )}

Próximas Reservas

{bookings.slice(0, 4).map(booking => (
{booking.facility === 'gym' ? : booking.facility === 'hall' ? : }

{booking.facilityName}

{booking.date} • {booking.time}

{booking.resident}

))}

Quadro de Avisos

{issues.slice(0, 3).map((issue) => (
{issue.date}

{issue.title}

{issue.location}

))}
); const BookingView = ({ facilityType, title, icon: Icon, description, priceInfo, color }) => { const facilityBookings = bookings.filter(b => b.facility === facilityType); return (

{title}

{description}

Horário: 08:00 - 22:00 {priceInfo}

Agenda de Reservas

{facilityBookings.length === 0 ? (

Sem reservas agendadas para este espaço.

) : (
{facilityBookings.map(booking => (
{booking.date}

{booking.time}

{booking.resident}

{booking.cost > 0 && (
Custo: {booking.cost}€
)}
))}
)}
); }; const MapView = () => { const [activePoint, setActivePoint] = useState(null); const [espacos, setEspacos] = useState([]); const [route, setRoute] = useState(null); const [isLocating, setIsLocating] = useState(false); const IconMap = { Building2, ShoppingCart, Store, HeartPulse, Info, MapPin, Trophy, Dumbbell, PartyPopper, Waves }; useEffect(() => { const defaultEspacos = { 'bloco-a': { nome: 'Bloco A', tipo: 'Residencial', descricao: '10 andares • 20 Frações', icone: 'Building2', color: 'text-orange-500', bg: 'bg-orange-100 dark:bg-orange-900/40', border: 'border-orange-300 dark:border-orange-700/50', x: 8, y: 15, w: 20, h: 28, canBook: false, latitude: 38.7225, longitude: -9.1398 }, 'bloco-b': { nome: 'Bloco B', tipo: 'Residencial', descricao: '8 andares • 16 Frações', icone: 'Building2', color: 'text-orange-500', bg: 'bg-orange-100 dark:bg-orange-900/40', border: 'border-orange-300 dark:border-orange-700/50', x: 8, y: 55, w: 20, h: 28, canBook: false, latitude: 38.7215, longitude: -9.1398 }, 'mercado-1': { nome: 'Mini Mercado 1', tipo: 'Comércio', descricao: 'Bens de primeira necessidade', icone: 'ShoppingCart', color: 'text-amber-600', bg: 'bg-amber-100 dark:bg-amber-900/40', border: 'border-amber-300 dark:border-amber-700/50', x: 32, y: 20, w: 12, h: 15, canBook: false, latitude: 38.7228, longitude: -9.1390 }, 'mercado-2': { nome: 'Mini Mercado 2', tipo: 'Comércio', descricao: 'Mercearia e cafetaria', icone: 'Store', color: 'text-amber-600', bg: 'bg-amber-100 dark:bg-amber-900/40', border: 'border-amber-300 dark:border-amber-700/50', x: 32, y: 65, w: 12, h: 15, canBook: false, latitude: 38.7212, longitude: -9.1390 }, 'medico': { nome: 'Posto Médico', tipo: 'Serviços', descricao: 'Primeiros socorros e saúde', icone: 'HeartPulse', color: 'text-red-600', bg: 'bg-red-100 dark:bg-red-900/40', border: 'border-red-300 dark:border-red-700/50', x: 48, y: 20, w: 12, h: 15, canBook: false, latitude: 38.7228, longitude: -9.1385 }, 'reception': { nome: 'Recepção', tipo: 'Serviços', descricao: 'Segurança 24h e Encomendas', icone: 'Info', color: 'text-slate-700 dark:text-slate-300', bg: 'bg-slate-200 dark:bg-slate-700', border: 'border-slate-400 dark:border-slate-500', x: 48, y: 45, w: 8, h: 12, isRound: true, canBook: false, latitude: 38.7220, longitude: -9.1385 }, 'pool': { nome: 'Piscina', tipo: 'Lazer', descricao: 'Piscina exterior aquecida', icone: 'MapPin', color: 'text-teal-600', bg: 'bg-teal-100 dark:bg-teal-900/40', border: 'border-teal-300 dark:border-teal-700/50', x: 48, y: 65, w: 14, h: 18, canBook: false, latitude: 38.7212, longitude: -9.1385 }, 'park': { nome: 'Parque de Jogos', tipo: 'Lazer', descricao: 'Campo Polidesportivo', icone: 'Trophy', color: 'text-green-600', bg: 'bg-green-100 dark:bg-green-900/40', border: 'border-green-300 dark:border-green-700/50', x: 65, y: 15, w: 18, h: 25, canBook: true, bookId: 'park', latitude: 38.7225, longitude: -9.1375 }, 'gym': { nome: 'Ginásio', tipo: 'Lazer', descricao: 'Equipamento Cardio e Força', icone: 'Dumbbell', color: 'text-blue-600', bg: 'bg-blue-100 dark:bg-blue-900/40', border: 'border-blue-300 dark:border-blue-700/50', x: 65, y: 48, w: 14, h: 18, canBook: true, bookId: 'gym', latitude: 38.7218, longitude: -9.1375 }, 'hall': { nome: 'Salão Festas', type: 'Lazer', descricao: 'Capacidade 50 px', icone: 'PartyPopper', color: 'text-purple-600', bg: 'bg-purple-100 dark:bg-purple-900/40', border: 'border-purple-300 dark:border-purple-700/50', x: 65, y: 72, w: 14, h: 18, canBook: true, bookId: 'hall', latitude: 38.7210, longitude: -9.1375 }, 'deck': { nome: 'Deque do Rio', tipo: 'Lazer', descricao: 'Zona de relaxamento à beira rio', icone: 'Waves', color: 'text-cyan-700 dark:text-cyan-300', bg: 'bg-cyan-100 dark:bg-cyan-900/40', border: 'border-cyan-300 dark:border-cyan-700/50', x: 85, y: 40, w: 8, h: 30, canBook: false, latitude: 38.7220, longitude: -9.1360 }, }; const espacosRef = ref(db, 'espacos'); const unsub = onValue(espacosRef, (snapshot) => { if (snapshot.exists()) { const data = snapshot.val(); const loadedEspacos = Object.keys(data).map(key => ({ id: key, ...data[key] })); setEspacos(loadedEspacos); } else { // Seed inicial da base de dados se estiver vazia set(ref(db, 'espacos'), defaultEspacos).catch(console.error); } }); return () => unsub(); }, []); const getDistance = (lat1, lon1, lat2, lon2) => { const R = 6371; // Raio da Terra em km const dLat = (lat2 - lat1) * Math.PI / 180; const dLon = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon/2) * Math.sin(dLon/2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); return R * c; // em km }; const handleRoute = (espaco) => { setIsLocating(true); if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( (pos) => { const userLat = pos.coords.latitude; const userLng = pos.coords.longitude; const distKm = getDistance(userLat, userLng, espaco.latitude, espaco.longitude); // Mapeamento da localização GPS do utilizador para as coordenadas visuais (x,y) do mapa const minLat = 38.7205; const maxLat = 38.7230; const minLng = -9.1405; const maxLng = -9.1350; let userX = ((userLng - minLng) / (maxLng - minLng)) * 100; let userY = ((maxLat - userLat) / (maxLat - minLat)) * 100; // Se estiver fora do condomínio (> 5km), coloca o utilizador na entrada principal if (distKm > 5) { userX = 48; // Receção / Portaria userY = 95; // Entrada } else { userX = Math.max(5, Math.min(95, userX)); userY = Math.max(5, Math.min(95, userY)); } setRoute({ active: true, targetId: espaco.id, distance: distKm * 1000, walkTime: Math.max(1, Math.ceil((distKm / 5) * 60)), // 5 km/h a pé driveTime: Math.max(1, Math.ceil((distKm / 30) * 60)), // 30 km/h de carro userX, userY }); setIsLocating(false); }, (error) => { console.error("Erro de geolocalização:", error); alert("Não foi possível obter a sua localização. Verifique as permissões do browser."); setIsLocating(false); } ); } else { alert("Geolocalização não é suportada por este browser."); setIsLocating(false); } }; return (

Navegação Inteligente

Explore e encontre rotas no condomínio

Residencial Comércio Lazer Serviços
{/* Área do Mapa */}
{/* Visual River */}
RIO
VIA CENTRAL
{/* SVG Route Overlay */} {route && route.targetId && ( {espacos.filter(e => e.id === route.targetId).map(target => ( ))} )} {espacos.map(loc => { const IconComp = IconMap[loc.icone] || MapPin; return (
setActivePoint(loc.id)} onMouseEnter={() => setActivePoint(loc.id)} className={`absolute flex flex-col items-center justify-center cursor-pointer transition-all duration-300 border-2 ${loc.bg} ${loc.border} ${activePoint === loc.id ? 'scale-110 shadow-lg z-20 ring-4 ring-blue-400/30' : 'hover:scale-105 shadow-sm z-10'} ${loc.isRound ? 'rounded-full' : 'rounded-lg'}`} style={{ left: `${loc.x}%`, top: `${loc.y}%`, width: `${loc.w}%`, height: `${loc.h}%` }} > {!loc.isRound && ( {loc.nome} )}
); })}
{/* Tabela lateral / Legenda */}

Detalhes e Navegação

Selecione um ponto no mapa para ver rotas.

{espacos.map(loc => { const IconComp = IconMap[loc.icone] || MapPin; return (
setActivePoint(loc.id)} className={`p-3 rounded-xl cursor-pointer transition-all border ${activePoint === loc.id ? 'bg-blue-50 border-blue-200 dark:bg-blue-900/20 dark:border-blue-800 shadow-md transform scale-[1.02]' : 'bg-white border-slate-100 hover:border-slate-300 dark:bg-dark-card dark:border-dark-border hover:bg-slate-50 dark:hover:bg-dark-bg'}`} >
{loc.nome}
{loc.tipo}

{loc.descricao}

{activePoint === loc.id && (
{loc.canBook && ( )} {route && route.targetId === loc.id && (
Detalhes da Rota
Distância: {route.distance > 1000 ? (route.distance/1000).toFixed(1) + ' km' : Math.round(route.distance) + ' m'}
A pé: {route.walkTime} min
De carro: {route.driveTime} min
)}
)}
); })}
); }; const MaintenanceView = () => (

Manutenção e Ocorrências

Gestão de pedidos e reparações

{userRole === 'admin' ? ( ) : ( )}
{issues.length === 0 ? (

Sem ocorrências registadas.

) : (
{issues.map(issue => (
{issue.date}

{issue.title}

{issue.location}

Prioridade {issue.priority} {userRole === 'admin' && issue.status !== 'Resolvido' && ( )}
))}
)}
); const ProfileView = ({ theme, setTheme }) => { const [activeSection, setActiveSection] = useState('personal'); const isMorador = userRole !== 'admin'; const [formData, setFormData] = useState({ name: 'A carregar...', role: '...', email: '', contact: '', address: '' }); useEffect(() => { if (isMorador) { const currentUserData = residents.find(r => r.id === currentUserId) || {}; setFormData({ name: currentUserData.name || currentUserName || '', role: `Fração ${currentUserData.unit || 'N/A'}`, email: currentUserData.email || '', contact: currentUserData.contact || '', address: 'Morada do Condomínio' }); } else { const adminRef = ref(db, 'configuracoes/admin_profile'); const unsub = onValue(adminRef, (snapshot) => { if (snapshot.exists()) { setFormData(snapshot.val()); } else { setFormData({ name: 'Administrador do Condomínio', role: 'Síndico / Gestor', email: 'admin@mycondominium.pt', contact: '+351 912 345 678', address: 'Rua das Flores, nº 123, Escritório 2B' }); } }); return () => unsub(); } }, [residents, currentUserId, userRole, currentUserName, isMorador]); const handleChange = (field, value) => { setFormData(prev => ({ ...prev, [field]: value })); }; const [passwordData, setPasswordData] = useState({ current: '', new: '', confirm: '' }); const handlePasswordChange = (field, value) => { setPasswordData(prev => ({ ...prev, [field]: value })); }; const handleSavePassword = async () => { if (passwordData.new !== passwordData.confirm) { showNotification('As novas palavras-passe não coincidem.', 'error'); return; } if (passwordData.new.length < 4) { showNotification('A nova palavra-passe deve ter pelo menos 4 caracteres.', 'error'); return; } if (isMorador) { const currentUserData = residents.find(r => r.id === currentUserId); const currentPassword = currentUserData.password || currentUserData.contact || '1234'; if (passwordData.current !== currentPassword) { showNotification('A palavra-passe atual está incorreta.', 'error'); return; } try { await set(ref(db, `condominos/${currentUserData.id}/password`), passwordData.new); showNotification('Palavra-passe alterada com sucesso!', 'success'); setPasswordData({ current: '', new: '', confirm: '' }); sendSystemNotification('Um utilizador alterou a sua palavra-passe.', 'info', 'admin'); } catch (error) { console.error("Erro ao alterar palavra-passe:", error); showNotification('Erro ao alterar a palavra-passe.', 'error'); } } else { showNotification('A conta de administrador usa o Firebase Auth para gerir passwords.', 'info'); } }; const handleSave = async () => { if (isMorador) { const currentUserData = residents.find(r => r.id === currentUserId); if (currentUserData && currentUserData.id) { try { await set(ref(db, `condominos/${currentUserData.id}/email`), formData.email); await set(ref(db, `condominos/${currentUserData.id}/contact`), formData.contact); showNotification('Dados atualizados com sucesso!', 'success'); sendSystemNotification('Um utilizador atualizou os seus dados pessoais.', 'info', 'admin'); } catch (error) { console.error("Erro ao guardar os dados:", error); showNotification('Erro ao guardar os dados.', 'error'); } } } else { try { await set(ref(db, 'configuracoes/admin_profile'), formData); showNotification('Alterações guardadas com sucesso!', 'success'); } catch (error) { console.error("Erro ao guardar perfil admin:", error); showNotification('Erro ao guardar as alterações.', 'error'); } } }; return (
{/* Profile Sidebar */}
{userRole === 'admin' ? 'AD' : 'MO'}

{userRole === 'admin' ? 'Admin Condomínio' : 'Morador'}

{userRole === 'admin' ? 'Administrador Geral' : 'Residente'}

{userRole === 'admin' && ( )}
{/* Profile Content */}
{activeSection === 'personal' && (

Dados Pessoais

handleChange('name', e.target.value)} disabled={isMorador} /> handleChange('role', e.target.value)} disabled={isMorador} />
handleChange('email', e.target.value)} type="email" /> handleChange('contact', e.target.value)} />
handleChange('address', e.target.value)} disabled={isMorador} />
)} {activeSection === 'security' && (

Segurança

Autenticação de Dois Fatores (2FA)

Recomendamos ativar o 2FA para maior segurança da sua conta.

handlePasswordChange('current', e.target.value)} />
handlePasswordChange('new', e.target.value)} /> handlePasswordChange('confirm', e.target.value)} />
)} {activeSection === 'permissions' && (

Nível de Acesso

Acesso Total (Admin)

Tem permissões totais para gerir condóminos, finanças e configurações.

Permissões Específicas:

{['Gerir Condóminos (Criar, Editar, Eliminar)', 'Gestão Financeira Completa', 'Moderação de Ocorrências', 'Configuração do Sistema', 'Gestão de Usuários'].map((perm, idx) => (
{perm}
))}
)} {activeSection === 'settings' && (

Preferências da Aplicação

Notificações

Idioma da Aplicação

{[ { code: 'pt', label: 'Português', flag: '🇵🇹' }, { code: 'en', label: 'English', flag: '🇬🇧' }, { code: 'es', label: 'Español', flag: '🇪🇸' }, { code: 'fr', label: 'Français', flag: '🇫🇷' } ].map(lang => { const match = document.cookie.match(/googtrans=\/pt\/([a-z]{2})/); const activeLang = match ? match[1] : 'pt'; const isActive = activeLang === lang.code; return (
{ if(lang.code === 'pt') { document.cookie = "googtrans=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; document.cookie = "googtrans=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=" + window.location.hostname + "; path=/;"; } else { document.cookie = `googtrans=/pt/${lang.code}; path=/;`; document.cookie = `googtrans=/pt/${lang.code}; domain=${window.location.hostname}; path=/;`; } window.location.reload(); }} className={`border-2 p-3 rounded-lg text-center cursor-pointer transition-colors ${isActive ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/30' : 'border-slate-200 dark:border-dark-border bg-white dark:bg-dark-card hover:bg-slate-50 dark:hover:bg-dark-surface'}`} >
{lang.flag}
{lang.label}
); })}

A tradução é efetuada automaticamente e afetará toda a aplicação após recarregar a página.

Aparência

setTheme('light')} className={`border-2 ${theme === 'light' ? 'border-blue-500 bg-blue-50' : 'border-slate-200 bg-slate-50'} p-3 rounded-lg text-center cursor-pointer transition-colors`} >
Claro
setTheme('dark')} className={`border-2 ${theme === 'dark' ? 'border-blue-500 bg-blue-50' : 'border-slate-200 bg-slate-50'} p-3 rounded-lg text-center cursor-pointer transition-colors`} >
Escuro
setTheme('system')} className={`border-2 ${theme === 'system' ? 'border-blue-500 bg-blue-50' : 'border-slate-200 bg-slate-50'} p-3 rounded-lg text-center cursor-pointer transition-colors`} >
Sistema
)}
); }; if (!isAuthenticated) { return ; } return (
{/* Mobile Overlay */} {isSidebarOpen && (
)} {/* Sidebar */} {/* Main Content */}
{/* Header */}

{ 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' : activeTab === 'profile' ? 'O Meu Perfil' : activeTab }

{/* Notifications */}
{isNotificationsOpen && (

Notificações

{notificationsList.length === 0 ? (
Sem novas notificações
) : ( notificationsList.map(notif => (

{notif.message}

{notif.time}

{!notif.read && ( )}
)) )}
)}
setActiveTab('profile')} title="Meu Perfil" > {userRole === 'admin' ? 'AD' : 'MO'}
{/* Content Body */}
{/* --- DASHBOARD --- */} {activeTab === 'dashboard' && } {/* --- MAPA --- */} {activeTab === 'map' && } {/* --- GINÁSIO --- */} {activeTab === 'gym' && ( )} {/* --- SALÃO --- */} {activeTab === 'hall' && ( )} {/* --- PARQUE --- */} {activeTab === 'park' && ( )} {/* --- ALL BOOKINGS --- */} {activeTab === 'all_bookings' && (

Histórico de Reservas

Lista completa de agendamentos em todos os espaços de lazer

{bookings.map(booking => (
{booking.facility === 'gym' ? : booking.facility === 'hall' ? : }

{booking.facilityName}

{booking.date} • {booking.time}

{booking.resident}

))} {bookings.length === 0 && (

Sem reservas

Ainda não existem agendamentos no condomínio.

)}
)} {/* --- APPROVALS --- */} {activeTab === 'approvals' && userRole === 'admin' && (

Pagamentos Concluídos

Consulte o histórico de todos os pagamentos concluídos pelos condóminos.

{faturas.filter(f => f.status === 'Pago').map(fatura => ( ))} {faturas.filter(f => f.status === 'Pago').length === 0 && ( )}
Morador Fatura Estado Valor

{fatura.nomeMorador}

Fração: {fatura.fracao}

{fatura.categoria}

Venceu a: {fatura.dataVencimento}

Pago
{Number(fatura.valor).toFixed(2)}€
Nenhum pagamento concluído encontrado.
)} {/* --- RESIDENTS --- */} {activeTab === 'residents' && (

Gestão de Condóminos

Total: {residents.length} frações registadas

setSearchQuery(e.target.value)} className="pl-10 pr-4 py-2 border border-slate-200 dark:border-dark-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-full sm:w-64 bg-white dark:bg-dark-card dark:text-white dark:placeholder-slate-500 transition-colors" />
{filteredResidents.map((resident) => ( ))}
Fração Proprietário Contacto Estado Quotas Acesso Em Dívida Ações
{resident.unit}
{resident.name} {resident.email}
{resident.contact} 0 ? 'text-red-600 dark:text-red-400' : 'text-slate-600 dark:text-slate-400'}`}> {Number(resident.pending).toFixed(2)}€
)} {/* --- BILLING / COBRANÇAS --- */} {activeTab === 'billing' && userRole === 'admin' && (

Avisos de Cobrança

Emita faturas ou avise condóminos individualmente

{residents.map((resident) => ( ))}
Fração Condómino Quotas em Atraso Ações
{resident.unit} {resident.name} 0 ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-500'}`}> {resident.pending > 0 ? `${Number(resident.pending).toFixed(2)}€` : 'Regularizado'}
)} {/* --- MINHAS CONTAS (Morador) --- */} {activeTab === 'minhas_contas' && userRole === 'morador' && (

Total Pendente

{faturas.filter(f => f.moradorId === currentUserId && f.status === 'Pendente').reduce((acc, f) => acc + Number(f.valor), 0).toFixed(2)}€

Total Pago

{faturas.filter(f => f.moradorId === currentUserId && f.status === 'Pago').reduce((acc, f) => acc + Number(f.valor), 0).toFixed(2)}€

Minhas Faturas

Consulte as suas despesas e faturas emitidas

{faturas.filter(f => f.moradorId === currentUserId).length === 0 ? ( ) : ( faturas.filter(f => f.moradorId === currentUserId).map((fatura) => ( )) )}
Data Emissão Categoria Vencimento Valor Estado Ações
Nenhuma fatura encontrada.
{fatura.dataEmissao} {fatura.categoria} {fatura.dataVencimento} {Number(fatura.valor).toFixed(2)}€ {fatura.status === 'Pendente' ? ( ) : ( Pago )}
)} {/* --- FINANCES --- */} {/* --- FINANCES --- */} {activeTab === 'finance' && (

Receitas (Global)

{totalIncome.toFixed(2)}€

Despesas (Global)

{totalExpense.toFixed(2)}€

Balanço Líquido

= 0 ? 'text-blue-600 dark:text-blue-400' : 'text-red-600 dark:text-red-400'}`}> {balance > 0 ? '+' : ''}{balance.toFixed(2)}€

Diário Financeiro

{finances.length} movimentos
{finances.length === 0 && ( )}
{finances.map((item) => ( ))}
Data Categoria Descrição Tipo Valor Recibo
{item.date} {item.category} {item.desc} {item.type === 'income' ? '+' : '-'}{Number(item.amount).toFixed(2)}€
)} {/* --- MESSAGES --- */} {activeTab === 'messages' && (
{/* Contact List */}

Conversas

setActiveChat({ type: 'global', id: 'global', name: 'Fórum do Condomínio' })} className={`p-3 border-b-2 cursor-pointer transition-colors ${activeChat.type === 'global' ? 'border-blue-100 dark:border-blue-900/30 bg-blue-50/50 dark:bg-blue-900/10 hover:bg-blue-50/80 dark:hover:bg-blue-900/20' : 'border-transparent hover:bg-slate-50 dark:hover:bg-dark-card'}`} >

Fórum do Condomínio

Geral

Grupo partilhado

{chatGroups.filter(g => g.members && (Object.values(g.members).map(String).includes(String(currentUserId)) || userRole === 'admin')).map(group => (
setActiveChat({ type: 'group', id: group.id, name: group.name })} className={`p-3 border-b border-slate-50 dark:border-dark-border/50 cursor-pointer transition-colors ${activeChat.id === group.id ? 'bg-blue-50/50 dark:bg-blue-900/10' : 'hover:bg-slate-50 dark:hover:bg-dark-card'}`} >

{group.name}

{activeChat.id === group.id && }

Grupo

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

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

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

Morador

))}
{/* Chat Area */}
{activeChat.type === 'global' || activeChat.type === 'group' ? : activeChat.name.substring(0, 2).toUpperCase()}

{activeChat.name}

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

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

{msg.senderName}

)}

{msg.text}

{timeString}
); })}
{ e.preventDefault(); if (!newMessageText.trim()) return; try { const path = activeChat.type === 'global' ? 'mural_mensagens' : activeChat.type === 'group' ? `mensagens_grupo/${activeChat.id}` : `mensagens_privadas/${[currentUserId, activeChat.id].sort().join('_')}`; const newMsgRef = push(ref(db, path)); await set(newMsgRef, { text: newMessageText, senderId: currentUserId, senderName: currentUserName, role: userRole, timestamp: Date.now() }); setNewMessageText(''); } catch (error) { console.error("Erro ao enviar mensagem:", error); showNotification("Erro ao enviar mensagem.", "error"); } }} className="flex gap-2"> setNewMessageText(e.target.value)} placeholder="Escreva a sua mensagem..." className="flex-1 bg-slate-50 dark:bg-dark-bg border border-slate-200 dark:border-dark-border rounded-full px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:text-white" />
)} {/* --- MAINTENANCE --- */} {activeTab === 'maintenance' && } {/* --- PROFILE --- */} {activeTab === 'profile' && }
{/* --- Toast Notification --- */} {notification && (
{notification.type === 'success' ? : } {notification.message}
)} {/* --- Modals --- */} {/* Resident Modal */}
{/* Emitir Fatura Modal */}
{/* Finance Modal */}
{/* Issue Modal */}
{/* Booking Modal */}
Custo Estimado: {formData.cost || 0}€
setIsCreateGroupModalOpen(false)} title="Criar Novo Grupo">
{ e.preventDefault(); if (!newGroupName.trim() || newGroupMembers.length === 0) { showNotification('Selecione um nome e pelo menos um membro.', 'warning'); return; } try { const groupId = 'grupo_' + Date.now(); const allMembers = [...newGroupMembers, currentUserId]; await set(ref(db, `grupos_chat/${groupId}`), { id: groupId, name: newGroupName, members: allMembers, createdBy: currentUserId, timestamp: Date.now() }); setIsCreateGroupModalOpen(false); setActiveChat({ type: 'group', id: groupId, name: newGroupName }); showNotification('Grupo criado com sucesso.', 'success'); } catch (err) { showNotification('Erro ao criar grupo.', 'error'); } }}> setNewGroupName(e.target.value)} required />
{residents.filter(r => r.id !== currentUserId).map(r => (
{ if (e.target.checked) setNewGroupMembers([...newGroupMembers, r.id]); else setNewGroupMembers(newGroupMembers.filter(id => id !== r.id)); }} className="w-4 h-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500" />
))}
); } const root = createRoot(document.getElementById('root')); root.render( );