diff --git a/.vscode/settings.json b/.vscode/settings.json index 85245c9..5b06ac3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "liveServer.settings.port": 5505 + "liveServer.settings.port": 5506 } \ No newline at end of file diff --git a/database.rules.json b/database.rules.json new file mode 100644 index 0000000..d2b8268 --- /dev/null +++ b/database.rules.json @@ -0,0 +1,6 @@ +{ + "rules": { + ".read": true, + ".write": true + } +} diff --git a/firebase.js b/firebase.js index ea2dee2..9ffb439 100644 --- a/firebase.js +++ b/firebase.js @@ -3,7 +3,7 @@ import { getDatabase } from "https://www.gstatic.com/firebasejs/12.1.0/firebase- const firebaseConfig = { apiKey: "AQUI_TUA_API_KEY", - authDomain: "AQUI.firebaseapp.com", + authDomain: "condomaster-pro-ed9af.firebaseapp.com", databaseURL: "https://condomaster-pro-ed9af-default-rtdb.europe-west1.firebasedatabase.app", projectId: "condomaster-pro-ed9af", storageBucket: "condomaster-pro-ed9af.appspot.com", @@ -14,5 +14,5 @@ const firebaseConfig = { const app = initializeApp(firebaseConfig); const db = getDatabase(app); -export { app, db }; +export { app, db, firebaseConfig }; diff --git a/firebase.json b/firebase.json index 1edf059..a62dc34 100644 --- a/firebase.json +++ b/firebase.json @@ -1,4 +1,7 @@ { + "database": { + "rules": "database.rules.json" + }, "hosting": { "target": "condomaster", "public": ".", diff --git a/index.html b/index.html index 6695a01..bf3fe80 100644 --- a/index.html +++ b/index.html @@ -105,9 +105,9 @@ MessageCircle, Paperclip, Send, Store, HeartPulse, Waves, ShoppingCart, Navigation, Car, Home, Anchor, Fuel } from 'lucide-react'; - import { app } from './firebase.js'; + import { app, db } 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'; + import { ref, push, set, onValue, onChildAdded, onChildChanged, onChildRemoved, remove, update, get } from 'https://www.gstatic.com/firebasejs/12.1.0/firebase-database.js'; const translations = { "pt": { @@ -1041,7 +1041,7 @@ class ErrorBoundary extends React.Component {
{this.state.errorInfo && this.state.errorInfo.componentStack} - + ); } @@ -1050,40 +1050,130 @@ class ErrorBoundary extends React.Component { } const auth = getAuth(app); - const db = getDatabase(app); + + const DB_STATUS = { + PAGO: 'Pago', + PENDENTE: 'Pendente', + ATRASADO: 'Atrasado', + RESOLVIDO: 'Resolvido', + EM_PROGRESSO: 'Em Progresso', + EM_VALIDACAO: 'Em Validação', + NOVO: 'Novo', + CONFIRMADO: 'Confirmado', + }; + + const STATUS_ALIASES = { + 'paid': DB_STATUS.PAGO, 'pagado': DB_STATUS.PAGO, 'payé': DB_STATUS.PAGO, 'paye': DB_STATUS.PAGO, 'pago': DB_STATUS.PAGO, + 'pending': DB_STATUS.PENDENTE, 'pendiente': DB_STATUS.PENDENTE, 'en attente': DB_STATUS.PENDENTE, 'pendente': DB_STATUS.PENDENTE, + 'resolved': DB_STATUS.RESOLVIDO, 'resuelto': DB_STATUS.RESOLVIDO, 'résolu': DB_STATUS.RESOLVIDO, 'resolvido': DB_STATUS.RESOLVIDO, + 'in progress': DB_STATUS.EM_PROGRESSO, 'en progreso': DB_STATUS.EM_PROGRESSO, 'em progresso': DB_STATUS.EM_PROGRESSO, + 'overdue': DB_STATUS.ATRASADO, 'atrasado': DB_STATUS.ATRASADO, + 'confirmed': DB_STATUS.CONFIRMADO, 'confirmado': DB_STATUS.CONFIRMADO, + 'new': DB_STATUS.NOVO, 'nuevo': DB_STATUS.NOVO, 'nouveau': DB_STATUS.NOVO, 'novo': DB_STATUS.NOVO, + }; + + const normalizeStatus = (status) => { + if (!status) return status; + const lower = String(status).trim().toLowerCase(); + return STATUS_ALIASES[lower] || status; + }; + + const isPaidStatus = (status) => normalizeStatus(status) === DB_STATUS.PAGO; + const isPendingStatus = (status) => normalizeStatus(status) === DB_STATUS.PENDENTE; + const isResolvedStatus = (status) => normalizeStatus(status) === DB_STATUS.RESOLVIDO; + + const normalizeRecord = (path, id, val) => { + if (!val || typeof val !== 'object') return { id, ...(val || {}) }; + switch (path) { + case 'condominos': + return { + id, + unit: val.unit || val.fracao || '', + name: val.name || val.proprietario || '', + contact: val.contact || val.contacto || '', + email: val.email || '', + password: val.password, + photoUrl: val.photoUrl, + status: normalizeStatus(val.status || val.estado) || DB_STATUS.PAGO, + pending: Number(val.pending ?? val.divida ?? 0), + role: val.role || 'morador', + }; + case 'faturas': { + const normalizedStatus = val.status === DB_STATUS.EM_VALIDACAO ? DB_STATUS.PAGO : normalizeStatus(val.status); + return { id, ...val, status: normalizedStatus || val.status }; + } + case 'financas': + return { + id, + type: val.type || (val.tipo === 'receita' ? 'income' : val.tipo === 'despesa' ? 'expense' : val.type) || 'expense', + category: val.category || val.categoria || '', + date: val.date || val.data || '', + amount: Number(val.amount ?? val.valor ?? 0), + desc: val.desc || val.descricao || '', + }; + case 'manutencao': + return { + id, + title: val.title || val.titulo || '', + location: val.location || val.local || '', + priority: val.priority || val.prioridade || 'Média', + status: normalizeStatus(val.status) || val.status || DB_STATUS.NOVO, + date: val.date || val.data || '', + moradorId: val.moradorId || val.morador_id || '', + }; + default: + return { id, ...val }; + } + }; + + const parseRealtimeSnapshot = (path, data, userRole, currentUserId, sortFunc = null) => { + let parsed = Object.entries(data).map(([id, val]) => normalizeRecord(path, id, val)); + + if (userRole !== 'admin' && (path === 'manutencao' || path === 'reservas')) { + parsed = parsed.filter(item => item.moradorId === currentUserId); + } + + if (sortFunc) parsed = [...parsed].sort(sortFunc); + return parsed; + }; + + const sortByDateDesc = (a, b) => new Date(b.date) - new Date(a.date); + const sortByVencimentoDesc = (a, b) => new Date(b.dataVencimento) - new Date(a.dataVencimento); + + const defaultTranslate = (key) => translations['pt']?.[key] || key; const INITIAL_RESIDENTS = [ - { id: 1, unit: '1º Esq', name: t('ana_silva'), contact: '912 345 678', email: 'ana.silva@email.com', status: 'Pago', pending: 0, role: 'morador' }, - { id: 2, unit: '1º Dto', name: t('carlos_santos'), contact: '965 432 109', email: 'carlos.s@email.com', status: 'Pendente', pending: 45.00, role: 'morador' }, - { id: 3, unit: '2º Esq', name: t('maria_pereira'), contact: '933 221 110', email: 'maria.p@email.com', status: 'Pago', pending: 0, role: 'morador' }, - { id: 4, unit: '2º Dto', name: t('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: t('sofia_costa'), contact: '922 334 455', email: 'sofia.c@email.com', status: 'Pago', pending: 0, role: 'morador' }, - ]; + { id: '1', unit: '1º Esq', name: 'Ana Silva', contact: '912 345 678', email: 'ana.silva@email.com', status: DB_STATUS.PAGO, pending: 0, role: 'morador' }, + { id: '2', unit: '1º Dto', name: 'Carlos Santos', contact: '965 432 109', email: 'carlos.s@email.com', status: DB_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: DB_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: DB_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: DB_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' }, + { 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: t('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' }, + { id: '1', title: 'Lâmpada fundida no Hall', location: 'R/C', status: DB_STATUS.NOVO, priority: 'Baixa', date: '2023-10-15' }, + { id: '2', title: 'Porta da garagem não fecha', location: 'Garagem -1', status: DB_STATUS.EM_PROGRESSO, priority: 'Alta', date: '2023-10-14' }, + { id: '3', title: 'Infiltração no teto', location: '3º Dto', status: DB_STATUS.RESOLVIDO, priority: 'Média', date: '2023-10-10' }, ]; const INITIAL_BOOKINGS = [ - { id: 1, facility: 'hall', facilityName: t('sal_o_de_festas'), date: '2023-10-25', time: '14:00 - 20:00', resident: t('ana_silva'), status: 'Confirmado', cost: 50 }, - { id: 2, facility: 'gym', facilityName: 'Ginásio', date: '2023-10-20', time: '09:00 - 10:00', resident: t('carlos_santos'), status: 'Confirmado', cost: 0 }, - { id: 3, facility: 'park', facilityName: t('parque_de_jogos'), date: '2023-10-22', time: '18:00 - 19:00', resident: t('sofia_costa'), status: 'Pendente', cost: 10 }, + { id: '1', facility: 'hall', facilityName: 'Salão de Festas', date: '2023-10-25', time: '14:00 - 20:00', resident: 'Ana Silva', status: DB_STATUS.CONFIRMADO, cost: 50 }, + { id: '2', facility: 'gym', facilityName: 'Ginásio', date: '2023-10-20', time: '09:00 - 10:00', resident: 'Carlos Santos', status: DB_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: DB_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 }, + { 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 --- @@ -1240,22 +1330,22 @@ class ErrorBoundary extends React.Component { const Badge = ({ status }) => { - const { t, language, changeLanguage } = useTranslation(); + const { t } = useTranslation(); const styles = { - t('pago'): 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800', + [DB_STATUS.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', - t('resolvido'): 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800', - t('receita'): 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800', - t('confirmado'): 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800', - t('pendente'): 'bg-yellow-100 text-yellow-700 border-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400 dark:border-yellow-800', - t('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', - t('em_progresso'): 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800', - t('m_dia'): 'bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-900/30 dark:text-orange-400 dark:border-orange-800', - t('atrasado'): 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800', - t('despesa'): 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800', - t('alta'): 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800', - t('novo'): 'bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-900/30 dark:text-purple-400 dark:border-purple-800', - t('baixa'): 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800', + [DB_STATUS.RESOLVIDO]: 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800', + [DB_STATUS.PENDENTE]: 'bg-yellow-100 text-yellow-700 border-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400 dark:border-yellow-800', + [DB_STATUS.EM_VALIDACAO]: 'bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-900/30 dark:text-orange-400 dark:border-orange-800', + [DB_STATUS.EM_PROGRESSO]: 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800', + [DB_STATUS.ATRASADO]: 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800', + [DB_STATUS.CONFIRMADO]: 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800', + [DB_STATUS.NOVO]: 'bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-900/30 dark:text-purple-400 dark:border-purple-800', + 'Receita': 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-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', + 'Média': 'bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-900/30 dark:text-orange-400 dark:border-orange-800', + 'Baixa': 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800', }; const translateStatus = (s) => { @@ -1264,9 +1354,11 @@ class ErrorBoundary extends React.Component { return t(key); }; + const canonicalStatus = normalizeStatus(status); + return ( - - {translateStatus(status)} + + {translateStatus(canonicalStatus)} ); }; @@ -1482,24 +1574,23 @@ class ErrorBoundary extends React.Component { const [newGroupName, setNewGroupName] = useState(''); const [newGroupMembers, setNewGroupMembers] = useState([]); + const userRoleRef = useRef(userRole); + const currentUserIdRef = useRef(currentUserId); + userRoleRef.current = userRole; + currentUserIdRef.current = currentUserId; + 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 === t('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); + setter(parseRealtimeSnapshot( + path, + data, + userRoleRef.current, + currentUserIdRef.current, + sortFunc + )); } else { setter([]); } @@ -1507,20 +1598,56 @@ class ErrorBoundary extends React.Component { }; 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 unsubFinances = loadData('financas', setFinances, sortByDateDesc); + // Manutenção: carregar inicialmente e depois ligar listeners por child para atualizações em tempo real + const issuesRef = ref(db, 'manutencao'); + // initial load + get(issuesRef).then(snapshot => { + const data = snapshot.val(); + if (data) { + setIssues(parseRealtimeSnapshot('manutencao', data, userRoleRef.current, currentUserIdRef.current, sortByDateDesc)); + } else { + setIssues([]); + } + }).catch(err => console.error('Erro ao carregar manutencao inicial:', err)); + + const handleChildAdded = (snap) => { + const rec = normalizeRecord('manutencao', snap.key, snap.val()); + if (userRoleRef.current !== 'admin' && String(rec.moradorId) !== String(currentUserIdRef.current)) return; + setIssues(prev => { + const exists = prev.some(p => p.id === rec.id); + if (exists) return prev.map(p => p.id === rec.id ? rec : p).sort(sortByDateDesc); + return [rec, ...prev].sort(sortByDateDesc); + }); + }; + + const handleChildChanged = (snap) => { + const rec = normalizeRecord('manutencao', snap.key, snap.val()); + setIssues(prev => prev.map(p => p.id === rec.id ? rec : p).sort(sortByDateDesc)); + }; + + const handleChildRemoved = (snap) => { + setIssues(prev => prev.filter(p => p.id !== snap.key)); + }; + + const unsubIssuesAdded = onChildAdded(issuesRef, handleChildAdded, (err) => console.error('Erro onChildAdded manutencao:', err)); + const unsubIssuesChanged = onChildChanged(issuesRef, handleChildChanged, (err) => console.error('Erro onChildChanged manutencao:', err)); + const unsubIssuesRemoved = onChildRemoved(issuesRef, handleChildRemoved, (err) => console.error('Erro onChildRemoved manutencao:', err)); + const unsubBookings = loadData('reservas', setBookings, sortByDateDesc); + const unsubInvoices = loadData('faturacao', setInvoices, sortByDateDesc); + const unsubFaturas = loadData('faturas', setFaturas, sortByVencimentoDesc); const unsubGroups = loadData('grupos_chat', setChatGroups); const unsubAdmin = onValue(ref(db, 'configuracoes/admin_profile'), (snapshot) => { if (snapshot.exists()) setAdminProfile(snapshot.val()); - }); + }, (error) => console.error('Erro ao carregar perfil admin:', error)); return () => { unsubResidents(); unsubFinances(); - unsubIssues(); + // unsubscribe manutencao child listeners + try { unsubIssuesAdded(); } catch(e){/* ignore */} + try { unsubIssuesChanged(); } catch(e){/* ignore */} + try { unsubIssuesRemoved(); } catch(e){/* ignore */} unsubBookings(); unsubInvoices(); unsubFaturas(); @@ -1529,6 +1656,29 @@ class ErrorBoundary extends React.Component { }; }, []); + useEffect(() => { + if (!isAuthenticated) return; + + const refreshFilteredCollections = async () => { + const filteredPaths = [ + { path: 'manutencao', setter: setIssues, sort: sortByDateDesc }, + { path: 'reservas', setter: setBookings, sort: sortByDateDesc }, + ]; + + for (const { path, setter, sort } of filteredPaths) { + try { + const snapshot = await get(ref(db, path)); + const data = snapshot.val(); + setter(data ? parseRealtimeSnapshot(path, data, userRole, currentUserId, sort) : []); + } catch (error) { + console.error(`Erro ao atualizar ${path}:`, error); + } + } + }; + + refreshFilteredCollections(); + }, [isAuthenticated, userRole, currentUserId]); + useEffect(() => { if (!isAuthenticated || !currentUserId) { setNotificationsList([]); @@ -1580,11 +1730,11 @@ class ErrorBoundary extends React.Component { const updates = {}; residents.forEach((resident) => { - const residentFaturas = faturas.filter(f => f.moradorId === resident.id && f.status !== t('pago')); + const residentFaturas = faturas.filter(f => f.moradorId === resident.id && !isPaidStatus(f.status)); const actualPending = residentFaturas.reduce((acc, f) => acc + Number(f.valor), 0); - const actualStatus = actualPending > 0 ? t('pendente') : 'Pago'; + const actualStatus = actualPending > 0 ? DB_STATUS.PENDENTE : DB_STATUS.PAGO; - if (Number(resident.pending) !== actualPending || resident.status !== actualStatus) { + if (Number(resident.pending) !== actualPending || normalizeStatus(resident.status) !== actualStatus) { updates[`condominos/${resident.id}/pending`] = actualPending; updates[`condominos/${resident.id}/status`] = actualStatus; hasUpdates = true; @@ -1610,7 +1760,7 @@ class ErrorBoundary extends React.Component { const notificationRef = useRef(null); - const initialResidentForm = { unit: '', name: '', contact: '', email: '', status: 'Pago', pending: 0, role: 'morador' }; + const initialResidentForm = { unit: '', name: '', contact: '', email: '', status: DB_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 }; @@ -1655,22 +1805,32 @@ class ErrorBoundary extends React.Component { 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 !== t('resolvido')).length; + const activeIssuesCount = issues.filter(i => !isResolvedStatus(i.status)).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()) + (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 }; + const writeNotification = async (folder) => { + const newRef = push(ref(db, `notificacoes/${folder}`)); + await set(newRef, newNotif); + const targetFolder = userRole === 'admin' ? 'admin' : currentUserId; + if (folder === targetFolder) { + setNotificationsList(prev => [{ id: newRef.key, ...newNotif }, ...prev].sort((a, b) => b.timestamp - a.timestamp)); + } + }; + 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); + await Promise.all([ + ...residents.map(r => writeNotification(r.id)), + writeNotification('admin'), + ]); } else { - await push(ref(db, `notificacoes/${targetUserId}`), newNotif); + await writeNotification(targetUserId); } }; @@ -1740,6 +1900,7 @@ class ErrorBoundary extends React.Component { const newRole = resident.role === 'admin' ? 'morador' : 'admin'; const residentRef = ref(db, `condominos/${id}`); await set(residentRef, { ...resident, role: newRole }); + setResidents(prev => prev.map(r => r.id === id ? { ...r, role: newRole } : r)); showNotification(t('permiss_es_de_utilizador_atualizadas'), 'success'); } } catch (error) { @@ -1759,7 +1920,7 @@ class ErrorBoundary extends React.Component { name: formData.name || '', contact: formData.contact || '', email: formData.email || '', - status: formData.status || t('pago'), + status: normalizeStatus(formData.status) || DB_STATUS.PAGO, pending: Number(formData.pending) || 0, role: formData.role || 'morador' }; @@ -1767,20 +1928,23 @@ class ErrorBoundary extends React.Component { updatedData.password = formData.password; } await set(residentRef, updatedData); + setResidents(prev => prev.map(r => r.id === editingItem.id ? { ...updatedData } : r)); showNotification(`Condómino ${formData.name} atualizado`); } else { const residentsListRef = ref(db, 'condominos'); const newResidentRef = push(residentsListRef); - await set(newResidentRef, { + const newResident = { unit: formData.unit || '', name: formData.name || '', contact: formData.contact || '', email: formData.email || '', password: formData.password || '1234', - status: formData.status || t('pago'), + status: normalizeStatus(formData.status) || DB_STATUS.PAGO, pending: Number(formData.pending) || 0, role: formData.role || 'morador' - }); + }; + await set(newResidentRef, newResident); + setResidents(prev => [...prev, { id: newResidentRef.key, ...newResident }]); showNotification(`Novo condómino ${formData.name} adicionado`); } handleCloseModal(); @@ -1795,6 +1959,7 @@ class ErrorBoundary extends React.Component { try { const residentRef = ref(db, `condominos/${id}`); await remove(residentRef); + setResidents(prev => prev.filter(r => r.id !== id)); showNotification('Condómino removido', 'error'); } catch (error) { console.error("Erro ao eliminar no Firebase:", error); @@ -1812,7 +1977,9 @@ class ErrorBoundary extends React.Component { try { const amount = Number(formData.amount); const newFinanceRef = push(ref(db, 'financas')); - await set(newFinanceRef, { ...formData, amount }); + const newFinance = { ...formData, amount }; + await set(newFinanceRef, newFinance); + setFinances(prev => [{ id: newFinanceRef.key, ...newFinance }, ...prev].sort(sortByDateDesc)); if (formData.type === 'expense') { sendSystemNotification(`Nova despesa registada: ${formData.category} - ${amount.toFixed(2)}€`, 'warning', 'admin'); @@ -1836,7 +2003,8 @@ class ErrorBoundary extends React.Component { } try { const newIssueRef = push(ref(db, 'manutencao')); - await set(newIssueRef, { ...formData, moradorId: currentUserId }); + const newIssue = { ...formData, moradorId: currentUserId }; + await set(newIssueRef, newIssue); sendSystemNotification(`Nova ocorrência reportada: ${formData.title} (${formData.location})`, 'warning', 'admin'); if (userRole !== 'admin') { @@ -1858,27 +2026,34 @@ class ErrorBoundary extends React.Component { return; } try { - const morador = residents.find(r => r.id === formData.moradorId); - if (!morador) return; + // Comparação segura entre IDs (firebase keys são strings) + const morador = residents.find(r => String(r.id) === String(formData.moradorId)); + if (!morador) { + showNotification("Morador seleccionado não encontrado.", "error"); + return; + } const valor = Number(formData.valor); const newFaturaRef = push(ref(db, 'faturas')); - await set(newFaturaRef, { + const newFatura = { moradorId: morador.id, nomeMorador: morador.name, fracao: morador.unit, categoria: formData.categoria, valor: valor, dataVencimento: formData.dataVencimento, - status: 'Pendente', + status: DB_STATUS.PENDENTE, dataEmissao: new Date().toISOString().split('T')[0] - }); + }; + await set(newFaturaRef, newFatura); + setFaturas(prev => [{ id: newFaturaRef.key, ...newFatura }, ...prev].sort(sortByVencimentoDesc)); const newPending = (Number(morador.pending) || 0) + valor; await update(ref(db, `condominos/${morador.id}`), { pending: newPending, - status: 'Pendente' + status: DB_STATUS.PENDENTE }); + setResidents(prev => prev.map(r => r.id === morador.id ? { ...r, pending: newPending, status: DB_STATUS.PENDENTE } : r)); 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'); @@ -1893,16 +2068,19 @@ class ErrorBoundary extends React.Component { const handlePayFatura = async (fatura) => { try { - await set(ref(db, `faturas/${fatura.id}/status`), t('pago')); + await set(ref(db, `faturas/${fatura.id}/status`), DB_STATUS.PAGO); + setFaturas(prev => prev.map(f => f.id === fatura.id ? { ...f, status: DB_STATUS.PAGO } : f)); 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; + const newStatus = newPending === 0 ? DB_STATUS.PAGO : DB_STATUS.PENDENTE; await update(ref(db, `condominos/${morador.id}`), { pending: newPending, - status: newPending === 0 ? t('pago') : 'Pendente' + status: newStatus }); + setResidents(prev => prev.map(r => r.id === morador.id ? { ...r, pending: newPending, status: newStatus } : r)); } 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'); @@ -1915,16 +2093,19 @@ class ErrorBoundary extends React.Component { const handleApproveFatura = async (fatura) => { try { - await set(ref(db, `faturas/${fatura.id}/status`), t('pago')); + await set(ref(db, `faturas/${fatura.id}/status`), DB_STATUS.PAGO); + setFaturas(prev => prev.map(f => f.id === fatura.id ? { ...f, status: DB_STATUS.PAGO } : f)); 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; + const newStatus = newPending === 0 ? DB_STATUS.PAGO : DB_STATUS.PENDENTE; await update(ref(db, `condominos/${morador.id}`), { pending: newPending, - status: newPending === 0 ? t('pago') : 'Pendente' + status: newStatus }); + setResidents(prev => prev.map(r => r.id === morador.id ? { ...r, pending: newPending, status: newStatus } : r)); } 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'); @@ -1939,7 +2120,9 @@ class ErrorBoundary extends React.Component { try { const issue = issues.find(i => i.id === id); if (issue) { - await set(ref(db, `manutencao/${id}`), { ...issue, status: 'Resolvido' }); + const resolvedIssue = { ...issue, status: DB_STATUS.RESOLVIDO }; + await set(ref(db, `manutencao/${id}`), resolvedIssue); + setIssues(prev => prev.map(i => i.id === id ? resolvedIssue : i)); sendSystemNotification(`A manutenção "${issue.title}" foi concluída com sucesso.`, 'success', 'todos'); showNotification(t('ocorr_ncia_resolvida_com_sucesso')); } @@ -1960,22 +2143,25 @@ class ErrorBoundary extends React.Component { const bookingData = { ...formData, facilityName: facilityNames[formData.facility], - status: 'Confirmado', + status: DB_STATUS.CONFIRMADO, moradorId: currentUserId }; const newBookingRef = push(ref(db, 'reservas')); await set(newBookingRef, bookingData); + setBookings(prev => [{ id: newBookingRef.key, ...bookingData }, ...prev].sort(sortByDateDesc)); if (bookingData.cost > 0) { const newIncomeRef = push(ref(db, 'financas')); - await set(newIncomeRef, { + const newIncome = { type: 'income', category: `Reserva: ${bookingData.facilityName}`, date: bookingData.date, amount: bookingData.cost, desc: `Reserva por ${bookingData.resident}` - }); + }; + await set(newIncomeRef, newIncome); + setFinances(prev => [{ id: newIncomeRef.key, ...newIncome }, ...prev].sort(sortByDateDesc)); } sendSystemNotification(`Nova reserva: ${bookingData.facilityName} a ${bookingData.date}`, 'info', 'admin'); @@ -1998,14 +2184,16 @@ class ErrorBoundary extends React.Component { } try { const newInvoiceRef = push(ref(db, 'faturacao')); - await set(newInvoiceRef, { + const newInvoice = { residentId: resident.id, unit: resident.unit, name: resident.name, amount: Number(resident.pending), date: new Date().toISOString().split('T')[0], status: 'Emitida' - }); + }; + await set(newInvoiceRef, newInvoice); + setInvoices(prev => [{ id: newInvoiceRef.key, ...newInvoice }, ...prev].sort(sortByDateDesc)); 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'); @@ -2023,7 +2211,7 @@ class ErrorBoundary extends React.Component { {userRole === 'admin' ? ( = 0 ? 'up' : 'down'} trendValue="Atual" color="bg-blue-500" /> ) : ( - + )} @@ -2525,7 +2713,7 @@ class ErrorBoundary extends React.Component { Prioridade {issue.priority} - {userRole === 'admin' && issue.status !== t('resolvido') && ( + {userRole === 'admin' && !isResolvedStatus(issue.status) && (