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 = {
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 };

View File

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

View File

@@ -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 {
<br />
{this.state.errorInfo && this.state.errorInfo.componentStack}
</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>
);
}
@@ -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 (
<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'}`}>
{translateStatus(status)}
<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(canonicalStatus)}
</span>
);
};
@@ -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' ? (
<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('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}
</span>
{userRole === 'admin' && issue.status !== t('resolvido') && (
{userRole === 'admin' && !isResolvedStatus(issue.status) && (
<button
onClick={() => handleResolveIssue(issue.id)}
className="text-sm text-green-600 dark:text-green-400 font-medium hover:text-green-700 dark:hover:text-green-300 flex items-center gap-1"
@@ -2546,7 +2734,7 @@ class ErrorBoundary extends React.Component {
const [activeSection, setActiveSection] = useState('personal');
const isMorador = userRole !== 'admin';
const [formData, setFormData] = useState({
const [profileForm, setProfileForm] = useState({
name: 'A carregar...',
role: '...',
email: '',
@@ -2557,7 +2745,7 @@ class ErrorBoundary extends React.Component {
useEffect(() => {
if (isMorador) {
const currentUserData = residents.find(r => r.id === currentUserId) || {};
setFormData({
setProfileForm({
name: currentUserData.name || currentUserName || '',
role: `Fração ${currentUserData.unit || 'N/A'}`,
email: currentUserData.email || '',
@@ -2569,9 +2757,9 @@ class ErrorBoundary extends React.Component {
const adminRef = ref(db, 'configuracoes/admin_profile');
const unsub = onValue(adminRef, (snapshot) => {
if (snapshot.exists()) {
setFormData(snapshot.val());
setProfileForm(snapshot.val());
} else {
setFormData({
setProfileForm({
name: 'Administrador do Condomínio',
role: 'Síndico / Gestor',
email: 'admin@mycondominium.pt',
@@ -2585,7 +2773,7 @@ class ErrorBoundary extends React.Component {
}, [residents, currentUserId, userRole, currentUserName, isMorador]);
const handleChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
setProfileForm(prev => ({ ...prev, [field]: value }));
};
const [passwordData, setPasswordData] = useState({ current: '', new: '', confirm: '' });
@@ -2613,6 +2801,7 @@ class ErrorBoundary extends React.Component {
try {
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');
setPasswordData({ current: '', new: '', confirm: '' });
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);
if (currentUserData && currentUserData.id) {
try {
await set(ref(db, `condominos/${currentUserData.id}/email`), formData.email);
await set(ref(db, `condominos/${currentUserData.id}/contact`), formData.contact);
if (formData.photoUrl !== undefined) {
await set(ref(db, `condominos/${currentUserData.id}/photoUrl`), formData.photoUrl);
await set(ref(db, `condominos/${currentUserData.id}/email`), profileForm.email);
await set(ref(db, `condominos/${currentUserData.id}/contact`), profileForm.contact);
if (profileForm.photoUrl !== undefined) {
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');
sendSystemNotification('Um utilizador atualizou os seus dados pessoais.', 'info', 'admin');
} catch (error) {
@@ -2644,7 +2839,7 @@ class ErrorBoundary extends React.Component {
}
} else {
try {
await set(ref(db, 'configuracoes/admin_profile'), formData);
await set(ref(db, 'configuracoes/admin_profile'), profileForm);
showNotification('Alterações guardadas com sucesso!', 'success');
} catch (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"
onClick={() => fileInputRef.current && fileInputRef.current.click()}
>
{formData.photoUrl ? (
<img src={formData.photoUrl} alt="Perfil" className="w-full h-full object-cover" />
{profileForm.photoUrl ? (
<img src={profileForm.photoUrl} alt="Perfil" className="w-full h-full object-cover" />
) : (
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>
<div className="space-y-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={isMorador ? t('fra_o') : "Cargo"} value={formData.role} onChange={(e) => handleChange('role', 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={profileForm.role} onChange={(e) => handleChange('role', e.target.value)} disabled={isMorador} />
</div>
<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('telefone')} value={formData.contact} onChange={(e) => handleChange('contact', e.target.value)} />
<InputGroup label={t('email')} value={profileForm.email} onChange={(e) => handleChange('email', e.target.value)} type="email" />
<InputGroup label={t('telefone')} value={profileForm.contact} onChange={(e) => handleChange('contact', e.target.value)} />
</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">
<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
@@ -3155,7 +3350,7 @@ class ErrorBoundary extends React.Component {
</tr>
</thead>
<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">
<td className="p-4">
<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>
</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>
)}
</tbody>
@@ -3344,7 +3539,7 @@ class ErrorBoundary extends React.Component {
<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>
<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>
</div>
<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">
<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">
{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>
</div>
<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-center"><Badge status={fatura.status} /></td>
<td className="px-6 py-4 text-center">
{fatura.status === t('pendente') ? (
{isPendingStatus(fatura.status) ? (
<button
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"
@@ -3651,13 +3846,15 @@ class ErrorBoundary extends React.Component {
: `mensagens_privadas/${[currentUserId, activeChat.id].sort().join('_')}`;
const newMsgRef = push(ref(db, path));
await set(newMsgRef, {
const newMessage = {
text: newMessageText,
senderId: currentUserId,
senderName: currentUserName,
role: userRole,
timestamp: Date.now()
});
};
await set(newMsgRef, newMessage);
setMessages(prev => [...prev, { id: newMsgRef.key, ...newMessage }]);
setNewMessageText('');
} catch (error) {
console.error("Erro ao enviar mensagem:", error);
@@ -3880,7 +4077,9 @@ class ErrorBoundary extends React.Component {
const root = createRoot(document.getElementById('root'));
root.render(
<ErrorBoundary>
<App />
<LanguageProvider>
<App />
</LanguageProvider>
</ErrorBoundary>
);
</script>

View File

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