ajuste base de dados

This commit is contained in:
2026-06-11 17:06:13 +01:00
parent ff481bbcd5
commit ad2f430c8d
6 changed files with 344 additions and 134 deletions

View File

@@ -1,3 +1,3 @@
{ {
"liveServer.settings.port": 5505 "liveServer.settings.port": 5506
} }

6
database.rules.json Normal file
View File

@@ -0,0 +1,6 @@
{
"rules": {
".read": true,
".write": true
}
}

View File

@@ -3,7 +3,7 @@ import { getDatabase } from "https://www.gstatic.com/firebasejs/12.1.0/firebase-
const firebaseConfig = { const firebaseConfig = {
apiKey: "AQUI_TUA_API_KEY", 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", databaseURL: "https://condomaster-pro-ed9af-default-rtdb.europe-west1.firebasedatabase.app",
projectId: "condomaster-pro-ed9af", projectId: "condomaster-pro-ed9af",
storageBucket: "condomaster-pro-ed9af.appspot.com", storageBucket: "condomaster-pro-ed9af.appspot.com",
@@ -14,5 +14,5 @@ const firebaseConfig = {
const app = initializeApp(firebaseConfig); const app = initializeApp(firebaseConfig);
const db = getDatabase(app); const db = getDatabase(app);
export { app, db }; export { app, db, firebaseConfig };

View File

@@ -1,4 +1,7 @@
{ {
"database": {
"rules": "database.rules.json"
},
"hosting": { "hosting": {
"target": "condomaster", "target": "condomaster",
"public": ".", "public": ".",

View File

@@ -105,9 +105,9 @@
MessageCircle, Paperclip, Send, Store, HeartPulse, Waves, ShoppingCart, Navigation, Car, Home, Anchor, Fuel MessageCircle, Paperclip, Send, Store, HeartPulse, Waves, ShoppingCart, Navigation, Car, Home, Anchor, Fuel
} from 'lucide-react'; } 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 { 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 = { const translations = {
"pt": { "pt": {
@@ -1041,7 +1041,7 @@ class ErrorBoundary extends React.Component {
<br /> <br />
{this.state.errorInfo && this.state.errorInfo.componentStack} {this.state.errorInfo && this.state.errorInfo.componentStack}
</pre> </pre>
<button onClick={() => window.location.reload()} style={{ marginTop: '20px', padding: '10px 20px', backgroundColor: '#dc2626', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}>{t('recarregar_p_gina')}</button> <button onClick={() => window.location.reload()} style={{ marginTop: '20px', padding: '10px 20px', backgroundColor: '#dc2626', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}>{defaultTranslate('recarregar_p_gina')}</button>
</div> </div>
); );
} }
@@ -1050,40 +1050,130 @@ class ErrorBoundary extends React.Component {
} }
const auth = getAuth(app); 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 = [ 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: '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: t('carlos_santos'), contact: '965 432 109', email: 'carlos.s@email.com', status: 'Pendente', pending: 45.00, 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: t('maria_pereira'), contact: '933 221 110', email: 'maria.p@email.com', status: 'Pago', pending: 0, 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: t('jo_o_ferreira'), contact: '918 765 432', email: 'joao.f@email.com', status: 'Atrasado', pending: 135.00, 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: t('sofia_costa'), contact: '922 334 455', email: 'sofia.c@email.com', status: 'Pago', pending: 0, 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 = [ const INITIAL_FINANCES = [
{ id: 1, type: 'income', category: 'Quotas Mensais', date: '2023-10-01', amount: 2250.00, desc: 'Pagamento de quotas Outubro' }, { 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: '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: '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: '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: '5', type: 'expense', category: 'Jardinagem', date: '2023-10-12', amount: 85.00, desc: 'Poda de árvores' },
]; ];
const INITIAL_ISSUES = [ const INITIAL_ISSUES = [
{ id: 1, title: 'Lâmpada fundida no Hall', location: 'R/C', status: 'Novo', priority: 'Baixa', date: '2023-10-15' }, { 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: t('em_progresso'), priority: 'Alta', date: '2023-10-14' }, { 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: 'Resolvido', priority: 'Média', date: '2023-10-10' }, { id: '3', title: 'Infiltração no teto', location: '3º Dto', status: DB_STATUS.RESOLVIDO, priority: 'Média', date: '2023-10-10' },
]; ];
const INITIAL_BOOKINGS = [ 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: '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: t('carlos_santos'), status: 'Confirmado', cost: 0 }, { 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: t('parque_de_jogos'), date: '2023-10-22', time: '18:00 - 19:00', resident: t('sofia_costa'), status: 'Pendente', cost: 10 }, { 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 = [ const INITIAL_NOTIFICATIONS = [
{ id: 1, message: 'Nova reserva: Salão de Festas (25 Out)', time: 'Há 1 hora', type: 'info', 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: '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: '3', message: 'Manutenção urgente reportada', time: 'Há 5 horas', type: 'warning', read: false },
]; ];
// --- VALIDAÇÕES OFICIAIS --- // --- VALIDAÇÕES OFICIAIS ---
@@ -1240,22 +1330,22 @@ class ErrorBoundary extends React.Component {
const Badge = ({ status }) => { const Badge = ({ status }) => {
const { t, language, changeLanguage } = useTranslation(); const { t } = useTranslation();
const styles = { 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', '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', [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',
t('receita'): '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',
t('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.EM_VALIDACAO]: 'bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-900/30 dark:text-orange-400 dark:border-orange-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', [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',
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', [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',
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', [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',
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', [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',
t('atrasado'): 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800', 'Receita': 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-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', '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', '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', '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('baixa'): 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-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) => { const translateStatus = (s) => {
@@ -1264,9 +1354,11 @@ class ErrorBoundary extends React.Component {
return t(key); return t(key);
}; };
const canonicalStatus = normalizeStatus(status);
return ( return (
<span className={`px-2.5 py-1 rounded-full text-xs font-semibold border ${styles[status] || 'bg-gray-100 text-gray-700 border-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700'}`}> <span className={`px-2.5 py-1 rounded-full text-xs font-semibold border ${styles[canonicalStatus] || styles[status] || 'bg-gray-100 text-gray-700 border-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700'}`}>
{translateStatus(status)} {translateStatus(canonicalStatus)}
</span> </span>
); );
}; };
@@ -1482,24 +1574,23 @@ class ErrorBoundary extends React.Component {
const [newGroupName, setNewGroupName] = useState(''); const [newGroupName, setNewGroupName] = useState('');
const [newGroupMembers, setNewGroupMembers] = useState([]); const [newGroupMembers, setNewGroupMembers] = useState([]);
const userRoleRef = useRef(userRole);
const currentUserIdRef = useRef(currentUserId);
userRoleRef.current = userRole;
currentUserIdRef.current = currentUserId;
useEffect(() => { useEffect(() => {
const loadData = (path, setter, sortFunc = null) => { const loadData = (path, setter, sortFunc = null) => {
return onValue(ref(db, path), (snapshot) => { return onValue(ref(db, path), (snapshot) => {
const data = snapshot.val(); const data = snapshot.val();
if (data) { if (data) {
let parsed = Object.entries(data).map(([id, val]) => { setter(parseRealtimeSnapshot(
if (path === 'faturas' && val.status === t('em_valida_o')) { path,
return { id, ...val, status: 'Pago' }; data,
} userRoleRef.current,
return { id, ...val }; currentUserIdRef.current,
}); sortFunc
));
if (userRole !== 'admin' && (path === 'manutencao' || path === 'reservas')) {
parsed = parsed.filter(item => item.moradorId === currentUserId);
}
if (sortFunc) parsed = parsed.sort(sortFunc);
setter(parsed);
} else { } else {
setter([]); setter([]);
} }
@@ -1507,20 +1598,56 @@ class ErrorBoundary extends React.Component {
}; };
const unsubResidents = loadData('condominos', setResidents); const unsubResidents = loadData('condominos', setResidents);
const unsubFinances = loadData('financas', setFinances, (a,b) => new Date(b.date) - new Date(a.date)); const unsubFinances = loadData('financas', setFinances, sortByDateDesc);
const unsubIssues = loadData('manutencao', setIssues, (a,b) => new Date(b.date) - new Date(a.date)); // Manutenção: carregar inicialmente e depois ligar listeners por child para atualizações em tempo real
const unsubBookings = loadData('reservas', setBookings, (a,b) => new Date(a.date) - new Date(b.date)); const issuesRef = ref(db, 'manutencao');
const unsubInvoices = loadData('faturacao', setInvoices, (a,b) => new Date(b.date) - new Date(a.date)); // initial load
const unsubFaturas = loadData('faturas', setFaturas, (a,b) => new Date(b.dataVencimento) - new Date(a.dataVencimento)); 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 unsubGroups = loadData('grupos_chat', setChatGroups);
const unsubAdmin = onValue(ref(db, 'configuracoes/admin_profile'), (snapshot) => { const unsubAdmin = onValue(ref(db, 'configuracoes/admin_profile'), (snapshot) => {
if (snapshot.exists()) setAdminProfile(snapshot.val()); if (snapshot.exists()) setAdminProfile(snapshot.val());
}); }, (error) => console.error('Erro ao carregar perfil admin:', error));
return () => { return () => {
unsubResidents(); unsubResidents();
unsubFinances(); unsubFinances();
unsubIssues(); // unsubscribe manutencao child listeners
try { unsubIssuesAdded(); } catch(e){/* ignore */}
try { unsubIssuesChanged(); } catch(e){/* ignore */}
try { unsubIssuesRemoved(); } catch(e){/* ignore */}
unsubBookings(); unsubBookings();
unsubInvoices(); unsubInvoices();
unsubFaturas(); 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(() => { useEffect(() => {
if (!isAuthenticated || !currentUserId) { if (!isAuthenticated || !currentUserId) {
setNotificationsList([]); setNotificationsList([]);
@@ -1580,11 +1730,11 @@ class ErrorBoundary extends React.Component {
const updates = {}; const updates = {};
residents.forEach((resident) => { 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 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}/pending`] = actualPending;
updates[`condominos/${resident.id}/status`] = actualStatus; updates[`condominos/${resident.id}/status`] = actualStatus;
hasUpdates = true; hasUpdates = true;
@@ -1610,7 +1760,7 @@ class ErrorBoundary extends React.Component {
const notificationRef = useRef(null); 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 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 };
@@ -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 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 totalExpense = finances.filter(f => f.type === 'expense').reduce((acc, curr) => acc + Number(curr.amount), 0);
const balance = totalIncome - totalExpense; 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 unreadNotifications = notificationsList.filter(n => !n.read).length;
const filteredResidents = residents.filter(r => const filteredResidents = residents.filter(r =>
r.name.toLowerCase().includes(searchQuery.toLowerCase()) || (r.name || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
r.unit.toLowerCase().includes(searchQuery.toLowerCase()) (r.unit || '').toLowerCase().includes(searchQuery.toLowerCase())
); );
const sendSystemNotification = async (message, type = 'info', targetUserId = 'admin') => { const sendSystemNotification = async (message, type = 'info', targetUserId = 'admin') => {
const newNotif = { timestamp: Date.now(), message, time: 'Agora', type, read: false }; 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') { if (targetUserId === 'todos') {
const promises = residents.map(r => push(ref(db, `notificacoes/${r.id}`), newNotif)); await Promise.all([
promises.push(push(ref(db, `notificacoes/admin`), newNotif)); ...residents.map(r => writeNotification(r.id)),
await Promise.all(promises); writeNotification('admin'),
]);
} else { } 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 newRole = resident.role === 'admin' ? 'morador' : 'admin';
const residentRef = ref(db, `condominos/${id}`); const residentRef = ref(db, `condominos/${id}`);
await set(residentRef, { ...resident, role: newRole }); 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'); showNotification(t('permiss_es_de_utilizador_atualizadas'), 'success');
} }
} catch (error) { } catch (error) {
@@ -1759,7 +1920,7 @@ class ErrorBoundary extends React.Component {
name: formData.name || '', name: formData.name || '',
contact: formData.contact || '', contact: formData.contact || '',
email: formData.email || '', email: formData.email || '',
status: formData.status || t('pago'), status: normalizeStatus(formData.status) || DB_STATUS.PAGO,
pending: Number(formData.pending) || 0, pending: Number(formData.pending) || 0,
role: formData.role || 'morador' role: formData.role || 'morador'
}; };
@@ -1767,20 +1928,23 @@ class ErrorBoundary extends React.Component {
updatedData.password = formData.password; updatedData.password = formData.password;
} }
await set(residentRef, updatedData); await set(residentRef, updatedData);
setResidents(prev => prev.map(r => r.id === editingItem.id ? { ...updatedData } : r));
showNotification(`Condómino ${formData.name} atualizado`); showNotification(`Condómino ${formData.name} atualizado`);
} else { } else {
const residentsListRef = ref(db, 'condominos'); const residentsListRef = ref(db, 'condominos');
const newResidentRef = push(residentsListRef); const newResidentRef = push(residentsListRef);
await set(newResidentRef, { const newResident = {
unit: formData.unit || '', unit: formData.unit || '',
name: formData.name || '', name: formData.name || '',
contact: formData.contact || '', contact: formData.contact || '',
email: formData.email || '', email: formData.email || '',
password: formData.password || '1234', password: formData.password || '1234',
status: formData.status || t('pago'), status: normalizeStatus(formData.status) || DB_STATUS.PAGO,
pending: Number(formData.pending) || 0, pending: Number(formData.pending) || 0,
role: formData.role || 'morador' role: formData.role || 'morador'
}); };
await set(newResidentRef, newResident);
setResidents(prev => [...prev, { id: newResidentRef.key, ...newResident }]);
showNotification(`Novo condómino ${formData.name} adicionado`); showNotification(`Novo condómino ${formData.name} adicionado`);
} }
handleCloseModal(); handleCloseModal();
@@ -1795,6 +1959,7 @@ class ErrorBoundary extends React.Component {
try { try {
const residentRef = ref(db, `condominos/${id}`); const residentRef = ref(db, `condominos/${id}`);
await remove(residentRef); await remove(residentRef);
setResidents(prev => prev.filter(r => r.id !== id));
showNotification('Condómino removido', 'error'); showNotification('Condómino removido', 'error');
} catch (error) { } catch (error) {
console.error("Erro ao eliminar no Firebase:", error); console.error("Erro ao eliminar no Firebase:", error);
@@ -1812,7 +1977,9 @@ class ErrorBoundary extends React.Component {
try { try {
const amount = Number(formData.amount); const amount = Number(formData.amount);
const newFinanceRef = push(ref(db, 'financas')); 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') { if (formData.type === 'expense') {
sendSystemNotification(`Nova despesa registada: ${formData.category} - ${amount.toFixed(2)}`, 'warning', 'admin'); sendSystemNotification(`Nova despesa registada: ${formData.category} - ${amount.toFixed(2)}`, 'warning', 'admin');
@@ -1836,7 +2003,8 @@ class ErrorBoundary extends React.Component {
} }
try { try {
const newIssueRef = push(ref(db, 'manutencao')); 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'); sendSystemNotification(`Nova ocorrência reportada: ${formData.title} (${formData.location})`, 'warning', 'admin');
if (userRole !== 'admin') { if (userRole !== 'admin') {
@@ -1858,27 +2026,34 @@ class ErrorBoundary extends React.Component {
return; return;
} }
try { try {
const morador = residents.find(r => r.id === formData.moradorId); // Comparação segura entre IDs (firebase keys são strings)
if (!morador) return; 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 valor = Number(formData.valor);
const newFaturaRef = push(ref(db, 'faturas')); const newFaturaRef = push(ref(db, 'faturas'));
await set(newFaturaRef, { const newFatura = {
moradorId: morador.id, moradorId: morador.id,
nomeMorador: morador.name, nomeMorador: morador.name,
fracao: morador.unit, fracao: morador.unit,
categoria: formData.categoria, categoria: formData.categoria,
valor: valor, valor: valor,
dataVencimento: formData.dataVencimento, dataVencimento: formData.dataVencimento,
status: 'Pendente', status: DB_STATUS.PENDENTE,
dataEmissao: new Date().toISOString().split('T')[0] 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; const newPending = (Number(morador.pending) || 0) + valor;
await update(ref(db, `condominos/${morador.id}`), { await update(ref(db, `condominos/${morador.id}`), {
pending: newPending, 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(`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'); 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) => { const handlePayFatura = async (fatura) => {
try { 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); const morador = residents.find(r => r.id === fatura.moradorId);
if (morador) { if (morador) {
let newPending = (Number(morador.pending) || 0) - Number(fatura.valor); let newPending = (Number(morador.pending) || 0) - Number(fatura.valor);
if (newPending <= 0.01) newPending = 0; if (newPending <= 0.01) newPending = 0;
const newStatus = newPending === 0 ? DB_STATUS.PAGO : DB_STATUS.PENDENTE;
await update(ref(db, `condominos/${morador.id}`), { await update(ref(db, `condominos/${morador.id}`), {
pending: newPending, 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(`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'); 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) => { const handleApproveFatura = async (fatura) => {
try { 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); const morador = residents.find(r => r.id === fatura.moradorId);
if (morador) { if (morador) {
let newPending = (Number(morador.pending) || 0) - Number(fatura.valor); let newPending = (Number(morador.pending) || 0) - Number(fatura.valor);
if (newPending <= 0.01) newPending = 0; if (newPending <= 0.01) newPending = 0;
const newStatus = newPending === 0 ? DB_STATUS.PAGO : DB_STATUS.PENDENTE;
await update(ref(db, `condominos/${morador.id}`), { await update(ref(db, `condominos/${morador.id}`), {
pending: newPending, 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(`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'); 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 { try {
const issue = issues.find(i => i.id === id); const issue = issues.find(i => i.id === id);
if (issue) { 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'); sendSystemNotification(`A manutenção "${issue.title}" foi concluída com sucesso.`, 'success', 'todos');
showNotification(t('ocorr_ncia_resolvida_com_sucesso')); showNotification(t('ocorr_ncia_resolvida_com_sucesso'));
} }
@@ -1960,22 +2143,25 @@ class ErrorBoundary extends React.Component {
const bookingData = { const bookingData = {
...formData, ...formData,
facilityName: facilityNames[formData.facility], facilityName: facilityNames[formData.facility],
status: 'Confirmado', status: DB_STATUS.CONFIRMADO,
moradorId: currentUserId moradorId: currentUserId
}; };
const newBookingRef = push(ref(db, 'reservas')); const newBookingRef = push(ref(db, 'reservas'));
await set(newBookingRef, bookingData); await set(newBookingRef, bookingData);
setBookings(prev => [{ id: newBookingRef.key, ...bookingData }, ...prev].sort(sortByDateDesc));
if (bookingData.cost > 0) { if (bookingData.cost > 0) {
const newIncomeRef = push(ref(db, 'financas')); const newIncomeRef = push(ref(db, 'financas'));
await set(newIncomeRef, { const newIncome = {
type: 'income', type: 'income',
category: `Reserva: ${bookingData.facilityName}`, category: `Reserva: ${bookingData.facilityName}`,
date: bookingData.date, date: bookingData.date,
amount: bookingData.cost, amount: bookingData.cost,
desc: `Reserva por ${bookingData.resident}` 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'); sendSystemNotification(`Nova reserva: ${bookingData.facilityName} a ${bookingData.date}`, 'info', 'admin');
@@ -1998,14 +2184,16 @@ class ErrorBoundary extends React.Component {
} }
try { try {
const newInvoiceRef = push(ref(db, 'faturacao')); const newInvoiceRef = push(ref(db, 'faturacao'));
await set(newInvoiceRef, { const newInvoice = {
residentId: resident.id, residentId: resident.id,
unit: resident.unit, unit: resident.unit,
name: resident.name, name: resident.name,
amount: Number(resident.pending), amount: Number(resident.pending),
date: new Date().toISOString().split('T')[0], date: new Date().toISOString().split('T')[0],
status: 'Emitida' 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(`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'); 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' ? ( {userRole === 'admin' ? (
<Card title={t('saldo_dispon_vel')} value={`${balance.toFixed(2)}`} icon={Wallet} trend={balance >= 0 ? 'up' : 'down'} trendValue="Atual" color="bg-blue-500" /> <Card title={t('saldo_dispon_vel')} value={`${balance.toFixed(2)}`} icon={Wallet} trend={balance >= 0 ? 'up' : 'down'} trendValue="Atual" color="bg-blue-500" />
) : ( ) : (
<Card title={t('as_minhas_quotas')} value="Em Dia" icon={CheckCircle} trend="up" trendValue=t('pago') color="bg-green-500" subtitle={t('sem_valores_pendentes')} /> <Card title={t('as_minhas_quotas')} value="Em Dia" icon={CheckCircle} trend="up" trendValue={t('pago')} color="bg-green-500" subtitle={t('sem_valores_pendentes')} />
)} )}
<Card title={t('reservas_m_s')} value={bookings.length} icon={Calendar} trend="up" trendValue="+2" color="bg-purple-500" subtitle={t('total_agendado')} /> <Card title={t('reservas_m_s')} value={bookings.length} icon={Calendar} trend="up" trendValue="+2" color="bg-purple-500" subtitle={t('total_agendado')} />
<Card title={t('manuten_es_ativas')} value={activeIssuesCount} icon={Wrench} trend="up" trendValue="Novas" color="bg-orange-500" subtitle={t('em_resolu_o')} /> <Card title={t('manuten_es_ativas')} value={activeIssuesCount} icon={Wrench} trend="up" trendValue="Novas" color="bg-orange-500" subtitle={t('em_resolu_o')} />
@@ -2525,7 +2713,7 @@ class ErrorBoundary extends React.Component {
Prioridade {issue.priority} Prioridade {issue.priority}
</span> </span>
{userRole === 'admin' && issue.status !== t('resolvido') && ( {userRole === 'admin' && !isResolvedStatus(issue.status) && (
<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"
@@ -2546,7 +2734,7 @@ class ErrorBoundary extends React.Component {
const [activeSection, setActiveSection] = useState('personal'); const [activeSection, setActiveSection] = useState('personal');
const isMorador = userRole !== 'admin'; const isMorador = userRole !== 'admin';
const [formData, setFormData] = useState({ const [profileForm, setProfileForm] = useState({
name: 'A carregar...', name: 'A carregar...',
role: '...', role: '...',
email: '', email: '',
@@ -2557,7 +2745,7 @@ class ErrorBoundary extends React.Component {
useEffect(() => { useEffect(() => {
if (isMorador) { if (isMorador) {
const currentUserData = residents.find(r => r.id === currentUserId) || {}; const currentUserData = residents.find(r => r.id === currentUserId) || {};
setFormData({ setProfileForm({
name: currentUserData.name || currentUserName || '', name: currentUserData.name || currentUserName || '',
role: `Fração ${currentUserData.unit || 'N/A'}`, role: `Fração ${currentUserData.unit || 'N/A'}`,
email: currentUserData.email || '', email: currentUserData.email || '',
@@ -2569,9 +2757,9 @@ class ErrorBoundary extends React.Component {
const adminRef = ref(db, 'configuracoes/admin_profile'); const adminRef = ref(db, 'configuracoes/admin_profile');
const unsub = onValue(adminRef, (snapshot) => { const unsub = onValue(adminRef, (snapshot) => {
if (snapshot.exists()) { if (snapshot.exists()) {
setFormData(snapshot.val()); setProfileForm(snapshot.val());
} else { } else {
setFormData({ setProfileForm({
name: 'Administrador do Condomínio', name: 'Administrador do Condomínio',
role: 'Síndico / Gestor', role: 'Síndico / Gestor',
email: 'admin@mycondominium.pt', email: 'admin@mycondominium.pt',
@@ -2585,7 +2773,7 @@ class ErrorBoundary extends React.Component {
}, [residents, currentUserId, userRole, currentUserName, isMorador]); }, [residents, currentUserId, userRole, currentUserName, isMorador]);
const handleChange = (field, value) => { const handleChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value })); setProfileForm(prev => ({ ...prev, [field]: value }));
}; };
const [passwordData, setPasswordData] = useState({ current: '', new: '', confirm: '' }); const [passwordData, setPasswordData] = useState({ current: '', new: '', confirm: '' });
@@ -2613,6 +2801,7 @@ class ErrorBoundary extends React.Component {
try { try {
await set(ref(db, `condominos/${currentUserData.id}/password`), passwordData.new); await set(ref(db, `condominos/${currentUserData.id}/password`), passwordData.new);
setResidents(prev => prev.map(r => r.id === currentUserData.id ? { ...r, password: passwordData.new } : r));
showNotification('Palavra-passe alterada com sucesso!', 'success'); showNotification('Palavra-passe alterada com sucesso!', 'success');
setPasswordData({ current: '', new: '', confirm: '' }); setPasswordData({ current: '', new: '', confirm: '' });
sendSystemNotification('Um utilizador alterou a sua palavra-passe.', 'info', 'admin'); sendSystemNotification('Um utilizador alterou a sua palavra-passe.', 'info', 'admin');
@@ -2630,11 +2819,17 @@ class ErrorBoundary extends React.Component {
const currentUserData = residents.find(r => r.id === currentUserId); const currentUserData = residents.find(r => r.id === currentUserId);
if (currentUserData && currentUserData.id) { if (currentUserData && currentUserData.id) {
try { try {
await set(ref(db, `condominos/${currentUserData.id}/email`), formData.email); await set(ref(db, `condominos/${currentUserData.id}/email`), profileForm.email);
await set(ref(db, `condominos/${currentUserData.id}/contact`), formData.contact); await set(ref(db, `condominos/${currentUserData.id}/contact`), profileForm.contact);
if (formData.photoUrl !== undefined) { if (profileForm.photoUrl !== undefined) {
await set(ref(db, `condominos/${currentUserData.id}/photoUrl`), formData.photoUrl); await set(ref(db, `condominos/${currentUserData.id}/photoUrl`), profileForm.photoUrl);
} }
setResidents(prev => prev.map(r => r.id === currentUserData.id ? {
...r,
email: profileForm.email,
contact: profileForm.contact,
...(profileForm.photoUrl !== undefined ? { photoUrl: profileForm.photoUrl } : {})
} : r));
showNotification('Dados atualizados com sucesso!', 'success'); showNotification('Dados atualizados com sucesso!', 'success');
sendSystemNotification('Um utilizador atualizou os seus dados pessoais.', 'info', 'admin'); sendSystemNotification('Um utilizador atualizou os seus dados pessoais.', 'info', 'admin');
} catch (error) { } catch (error) {
@@ -2644,7 +2839,7 @@ class ErrorBoundary extends React.Component {
} }
} else { } else {
try { try {
await set(ref(db, 'configuracoes/admin_profile'), formData); await set(ref(db, 'configuracoes/admin_profile'), profileForm);
showNotification('Alterações guardadas com sucesso!', 'success'); showNotification('Alterações guardadas com sucesso!', 'success');
} catch (error) { } catch (error) {
console.error("Erro ao guardar perfil admin:", error); console.error("Erro ao guardar perfil admin:", error);
@@ -2675,8 +2870,8 @@ class ErrorBoundary extends React.Component {
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 cursor-pointer relative group overflow-hidden transition-all" 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 cursor-pointer relative group overflow-hidden transition-all"
onClick={() => fileInputRef.current && fileInputRef.current.click()} onClick={() => fileInputRef.current && fileInputRef.current.click()}
> >
{formData.photoUrl ? ( {profileForm.photoUrl ? (
<img src={formData.photoUrl} alt="Perfil" className="w-full h-full object-cover" /> <img src={profileForm.photoUrl} alt="Perfil" className="w-full h-full object-cover" />
) : ( ) : (
userRole === 'admin' ? 'AD' : 'MO' userRole === 'admin' ? 'AD' : 'MO'
)} )}
@@ -2730,14 +2925,14 @@ class ErrorBoundary extends React.Component {
<h3 className="text-lg font-bold text-slate-800 dark:text-white mb-6 pb-2 border-b border-slate-100 dark:border-dark-border">{t('dados_pessoais')}</h3> <h3 className="text-lg font-bold text-slate-800 dark:text-white mb-6 pb-2 border-b border-slate-100 dark:border-dark-border">{t('dados_pessoais')}</h3>
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<InputGroup label={t('nome_completo')} value={formData.name} onChange={(e) => handleChange('name', e.target.value)} disabled={isMorador} /> <InputGroup label={t('nome_completo')} value={profileForm.name} onChange={(e) => handleChange('name', e.target.value)} disabled={isMorador} />
<InputGroup label={isMorador ? t('fra_o') : "Cargo"} value={formData.role} onChange={(e) => handleChange('role', e.target.value)} disabled={isMorador} /> <InputGroup label={isMorador ? t('fra_o') : "Cargo"} value={profileForm.role} onChange={(e) => handleChange('role', e.target.value)} disabled={isMorador} />
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<InputGroup label={t('email')} value={formData.email} onChange={(e) => handleChange('email', e.target.value)} type="email" /> <InputGroup label={t('email')} value={profileForm.email} onChange={(e) => handleChange('email', e.target.value)} type="email" />
<InputGroup label={t('telefone')} value={formData.contact} onChange={(e) => handleChange('contact', e.target.value)} /> <InputGroup label={t('telefone')} value={profileForm.contact} onChange={(e) => handleChange('contact', e.target.value)} />
</div> </div>
<InputGroup label={isMorador ? "Morada" : "Morada (Sede)"} value={formData.address} onChange={(e) => handleChange('address', e.target.value)} disabled={isMorador} /> <InputGroup label={isMorador ? "Morada" : "Morada (Sede)"} value={profileForm.address} onChange={(e) => handleChange('address', e.target.value)} disabled={isMorador} />
<div className="flex justify-end mt-6"> <div className="flex justify-end mt-6">
<button onClick={handleSave} className="bg-blue-600 text-white px-6 py-2 rounded-lg font-medium hover:bg-blue-700 shadow-sm transition-colors"> <button onClick={handleSave} 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
@@ -3155,7 +3350,7 @@ class ErrorBoundary extends React.Component {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{faturas.filter(f => f.status === t('pago')).map(fatura => ( {faturas.filter(f => isPaidStatus(f.status)).map(fatura => (
<tr key={fatura.id} className="border-b border-slate-50 dark:border-dark-border hover:bg-slate-50/50 dark:hover:bg-dark-bg/50"> <tr key={fatura.id} className="border-b border-slate-50 dark:border-dark-border hover:bg-slate-50/50 dark:hover:bg-dark-bg/50">
<td className="p-4"> <td className="p-4">
<p className="font-semibold text-slate-700 dark:text-slate-200">{fatura.nomeMorador}</p> <p className="font-semibold text-slate-700 dark:text-slate-200">{fatura.nomeMorador}</p>
@@ -3173,7 +3368,7 @@ class ErrorBoundary extends React.Component {
<td className="p-4 font-bold text-green-600 dark:text-green-400 text-right">{Number(fatura.valor).toFixed(2)}</td> <td className="p-4 font-bold text-green-600 dark:text-green-400 text-right">{Number(fatura.valor).toFixed(2)}</td>
</tr> </tr>
))} ))}
{faturas.filter(f => f.status === t('pago')).length === 0 && ( {faturas.filter(f => isPaidStatus(f.status)).length === 0 && (
<tr><td colSpan="4" className="p-8 text-center text-slate-500">Nenhum pagamento concluído encontrado.</td></tr> <tr><td colSpan="4" className="p-8 text-center text-slate-500">Nenhum pagamento concluído encontrado.</td></tr>
)} )}
</tbody> </tbody>
@@ -3344,7 +3539,7 @@ class ErrorBoundary extends React.Component {
<div className="relative z-10"> <div className="relative z-10">
<h4 className="text-slate-500 dark:text-dark-mute text-xs font-bold uppercase tracking-wider">{t('total_pendente')}</h4> <h4 className="text-slate-500 dark:text-dark-mute text-xs font-bold uppercase tracking-wider">{t('total_pendente')}</h4>
<p className="text-2xl font-bold text-slate-800 dark:text-white mt-1"> <p className="text-2xl font-bold text-slate-800 dark:text-white mt-1">
{faturas.filter(f => f.moradorId === currentUserId && f.status === t('pendente')).reduce((acc, f) => acc + Number(f.valor), 0).toFixed(2)} {faturas.filter(f => f.moradorId === currentUserId && isPendingStatus(f.status)).reduce((acc, f) => acc + Number(f.valor), 0).toFixed(2)}
</p> </p>
</div> </div>
<AlertCircle className="absolute right-4 bottom-4 text-orange-100 dark:text-orange-900/20" size={64} /> <AlertCircle className="absolute right-4 bottom-4 text-orange-100 dark:text-orange-900/20" size={64} />
@@ -3353,7 +3548,7 @@ class ErrorBoundary extends React.Component {
<div className="relative z-10"> <div className="relative z-10">
<h4 className="text-slate-500 dark:text-dark-mute text-xs font-bold uppercase tracking-wider">{t('total_pago')}</h4> <h4 className="text-slate-500 dark:text-dark-mute text-xs font-bold uppercase tracking-wider">{t('total_pago')}</h4>
<p className="text-2xl font-bold text-slate-800 dark:text-white mt-1"> <p className="text-2xl font-bold text-slate-800 dark:text-white mt-1">
{faturas.filter(f => f.moradorId === currentUserId && f.status === t('pago')).reduce((acc, f) => acc + Number(f.valor), 0).toFixed(2)} {faturas.filter(f => f.moradorId === currentUserId && isPaidStatus(f.status)).reduce((acc, f) => acc + Number(f.valor), 0).toFixed(2)}
</p> </p>
</div> </div>
<CheckCircle className="absolute right-4 bottom-4 text-green-100 dark:text-green-900/20" size={64} /> <CheckCircle className="absolute right-4 bottom-4 text-green-100 dark:text-green-900/20" size={64} />
@@ -3395,7 +3590,7 @@ class ErrorBoundary extends React.Component {
<td className="px-6 py-4 text-right font-medium text-slate-800 dark:text-white">{Number(fatura.valor).toFixed(2)}</td> <td className="px-6 py-4 text-right font-medium text-slate-800 dark:text-white">{Number(fatura.valor).toFixed(2)}</td>
<td className="px-6 py-4 text-center"><Badge status={fatura.status} /></td> <td className="px-6 py-4 text-center"><Badge status={fatura.status} /></td>
<td className="px-6 py-4 text-center"> <td className="px-6 py-4 text-center">
{fatura.status === t('pendente') ? ( {isPendingStatus(fatura.status) ? (
<button <button
onClick={() => handlePayFatura(fatura)} onClick={() => handlePayFatura(fatura)}
className="bg-blue-600 text-white px-4 py-1.5 rounded-lg text-xs font-bold hover:bg-blue-700 transition-colors shadow-sm" className="bg-blue-600 text-white px-4 py-1.5 rounded-lg text-xs font-bold hover:bg-blue-700 transition-colors shadow-sm"
@@ -3651,13 +3846,15 @@ class ErrorBoundary extends React.Component {
: `mensagens_privadas/${[currentUserId, activeChat.id].sort().join('_')}`; : `mensagens_privadas/${[currentUserId, activeChat.id].sort().join('_')}`;
const newMsgRef = push(ref(db, path)); const newMsgRef = push(ref(db, path));
await set(newMsgRef, { const newMessage = {
text: newMessageText, text: newMessageText,
senderId: currentUserId, senderId: currentUserId,
senderName: currentUserName, senderName: currentUserName,
role: userRole, role: userRole,
timestamp: Date.now() timestamp: Date.now()
}); };
await set(newMsgRef, newMessage);
setMessages(prev => [...prev, { id: newMsgRef.key, ...newMessage }]);
setNewMessageText(''); setNewMessageText('');
} catch (error) { } catch (error) {
console.error("Erro ao enviar mensagem:", error); console.error("Erro ao enviar mensagem:", error);
@@ -3880,7 +4077,9 @@ class ErrorBoundary extends React.Component {
const root = createRoot(document.getElementById('root')); const root = createRoot(document.getElementById('root'));
root.render( root.render(
<ErrorBoundary> <ErrorBoundary>
<LanguageProvider>
<App /> <App />
</LanguageProvider>
</ErrorBoundary> </ErrorBoundary>
); );
</script> </script>

View File

@@ -134,7 +134,7 @@ async function dbInsert(table, row) {
row.created_at = new Date().toISOString(); row.created_at = new Date().toISOString();
if (!row.id) row.id = Date.now().toString(); if (!row.id) row.id = Date.now().toString();
await set(ref(db, `${table}/${row.id}`), row); await set(ref(db, `${table}/${row.id}`, row);
return { data: [row], error: null }; return { data: [row], error: null };
} catch (error) { } catch (error) {
console.error("Erro no dbInsert:", error); console.error("Erro no dbInsert:", error);
@@ -307,12 +307,14 @@ async function saveMorador(e) {
try { try {
// Gerar um novo ID automaticamente usando push() // Gerar um novo ID automaticamente usando push()
const condominiosRef = ref(db, "condominos"); const condominiosRef = ref(db, "condominos");
await push(condominiosRef, { const newRef = push(condominiosRef);
fracao, await set(newRef, {
proprietario, unit: fracao,
contacto, name: proprietario,
estado: estado, contact: contacto,
divida: estado === "Pago" ? 0 : 50 // Lógica de exemplo status: estado,
pending: estado === "Pago" ? 0 : 50,
role: 'morador'
}); });
// Limpar o formulário // Limpar o formulário
@@ -354,10 +356,10 @@ function listenCondominos() {
Object.entries(data).forEach(([id, m]) => { Object.entries(data).forEach(([id, m]) => {
const tr = document.createElement('tr'); const tr = document.createElement('tr');
tr.innerHTML = ` tr.innerHTML = `
<td><div class="fw-bold">${m.proprietario || 'Sem Nome'}</div></td> <td><div class="fw-bold">${m.name || m.proprietario || 'Sem Nome'}</div></td>
<td>${m.fracao || '-'}</td> <td>${m.unit || m.fracao || '-'}</td>
<td>${m.contacto || '-'}</td> <td>${m.contact || m.contacto || '-'}</td>
<td>${m.estado || 'Pago'}</td> <td>${m.status || m.estado || 'Pago'}</td>
<td> <td>
<button class="btn btn-sm btn-outline-danger" onclick="deleteItem('condominos', '${id}')"> <button class="btn btn-sm btn-outline-danger" onclick="deleteItem('condominos', '${id}')">
Eliminar Eliminar