This commit is contained in:
2026-05-06 12:43:37 +01:00
parent 000c1cd721
commit f6b7a98471
9 changed files with 2509 additions and 73 deletions

View File

@@ -5,7 +5,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#0f172a">
<title>CondoMaster Pro</title>
<title>MyCondominium</title>
<link rel="manifest" href="./manifest.json">
<script src="https://cdn.tailwindcss.com"></script>
@@ -100,10 +100,41 @@
Dumbbell, PartyPopper, Trophy, Map, Calendar, MapPin, Info,
MessageCircle, Paperclip, Send
} from 'lucide-react';
import { app } from './firebase.js';
import { getAuth, signInWithEmailAndPassword, createUserWithEmailAndPassword } from 'https://www.gstatic.com/firebasejs/12.1.0/firebase-auth.js';
import { getDatabase, ref, push, set, onValue, remove, update } from 'https://www.gstatic.com/firebasejs/12.1.0/firebase-database.js';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
this.setState({ error, errorInfo });
console.error("ErrorBoundary caught an error:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: '20px', backgroundColor: '#fee2e2', color: '#991b1b', fontFamily: 'sans-serif', height: '100vh' }}>
<h1 style={{ fontSize: '24px', fontWeight: 'bold' }}>Algo correu mal (Erro na Aplicação)</h1>
<pre style={{ marginTop: '20px', whiteSpace: 'pre-wrap', backgroundColor: '#fef2f2', padding: '15px', border: '1px solid #f87171' }}>
{this.state.error && this.state.error.toString()}
<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' }}>Recarregar Página</button>
</div>
);
}
return this.props.children;
}
}
const auth = getAuth(app);
const db = getDatabase(app);
@@ -326,7 +357,7 @@
<div className="inline-flex p-3 bg-blue-100 dark:bg-blue-900/30 rounded-full mb-4 text-blue-600 dark:text-blue-400">
<Building2 size={32} />
</div>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white">CondoMaster<span className="text-blue-600">Pro</span></h1>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white">MyCondominium</h1>
<p className="text-slate-500 dark:text-gray-400 mt-2">Portal de Gestão</p>
</div>
@@ -501,7 +532,17 @@
return onValue(ref(db, path), (snapshot) => {
const data = snapshot.val();
if (data) {
let parsed = Object.entries(data).map(([id, val]) => ({ id, ...val }));
let parsed = Object.entries(data).map(([id, val]) => {
if (path === 'faturas' && val.status === 'Em Validação') {
return { id, ...val, status: 'Pago' };
}
return { id, ...val };
});
if (userRole !== 'admin' && (path === 'manutencao' || path === 'reservas')) {
parsed = parsed.filter(item => item.moradorId === currentUserId);
}
if (sortFunc) parsed = parsed.sort(sortFunc);
setter(parsed);
} else {
@@ -799,7 +840,7 @@
}
try {
const newIssueRef = push(ref(db, 'manutencao'));
await set(newIssueRef, { ...formData });
await set(newIssueRef, { ...formData, moradorId: currentUserId });
sendSystemNotification(`Nova ocorrência reportada: ${formData.title} (${formData.location})`, 'warning', 'admin');
if (userRole !== 'admin') {
@@ -853,10 +894,17 @@
const handlePayFatura = async (fatura) => {
try {
await set(ref(db, `faturas/${fatura.id}/status`), 'Em Validação');
sendSystemNotification(`O pagamento da sua fatura de ${fatura.categoria} foi submetido. Aguarda validação.`, 'info', fatura.moradorId);
sendSystemNotification(`Comprovativo recebido da fração ${fatura.fracao}.`, 'info', 'admin');
showNotification("Comprovativo enviado! A aguardar validação do administrador.", "success");
await set(ref(db, `faturas/${fatura.id}/status`), 'Pago');
const morador = residents.find(r => r.id === fatura.moradorId);
if (morador) {
let newPending = (Number(morador.pending) || 0) - Number(fatura.valor);
if (newPending < 0) newPending = 0;
await set(ref(db, `condominos/${morador.id}/pending`), newPending);
}
sendSystemNotification(`O pagamento da sua fatura de ${fatura.categoria} foi concluído!`, 'success', fatura.moradorId);
sendSystemNotification(`Pagamento registado para a fatura de ${fatura.categoria} da fração ${morador?.unit || fatura.fracao}.`, 'success', 'admin');
showNotification("Pagamento efetuado com sucesso!", "success");
} catch (error) {
console.error("Erro ao pagar fatura:", error);
showNotification("Erro ao processar pagamento.", "error");
@@ -907,7 +955,8 @@
const bookingData = {
...formData,
facilityName: facilityNames[formData.facility],
status: 'Confirmado'
status: 'Confirmado',
moradorId: currentUserId
};
const newBookingRef = push(ref(db, 'reservas'));
@@ -1269,7 +1318,7 @@
<InputGroup label="Cargo" value="Síndico / Gestor" disabled />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<InputGroup label="Email" value="admin@condomaster.pt" type="email" />
<InputGroup label="Email" value="admin@mycondominium.pt" type="email" />
<InputGroup label="Telefone" value="+351 912 345 678" />
</div>
<InputGroup label="Morada (Sede)" value="Rua das Flores, nº 123, Escritório 2B" />
@@ -1422,7 +1471,7 @@
<Building2 size={24} />
</div>
<div>
<h1 className="text-xl font-bold text-slate-800 dark:text-white">CondoMaster<span className="text-blue-600 dark:text-blue-400">Pro</span></h1>
<h1 className="text-xl font-bold text-slate-800 dark:text-white">MyCondominium</h1>
<p className="text-xs text-slate-400 dark:text-dark-mute">Portal de Gestão</p>
</div>
</div>
@@ -1639,8 +1688,8 @@
{activeTab === 'approvals' && userRole === 'admin' && (
<div className="p-6">
<div className="mb-6">
<h2 className="text-2xl font-bold text-slate-800 dark:text-white">Aprovações de Pagamentos</h2>
<p className="text-slate-500 dark:text-dark-mute">Valide ou rejeite pagamentos de faturas enviados pelos condóminos.</p>
<h2 className="text-2xl font-bold text-slate-800 dark:text-white">Pagamentos Concluídos</h2>
<p className="text-slate-500 dark:text-dark-mute">Consulte o histórico de todos os pagamentos concluídos pelos condóminos.</p>
</div>
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border overflow-hidden">
<div className="overflow-x-auto">
@@ -1649,12 +1698,12 @@
<tr className="bg-slate-50 dark:bg-dark-bg border-b border-slate-100 dark:border-dark-border text-sm font-semibold text-slate-500 dark:text-slate-400">
<th className="p-4">Morador</th>
<th className="p-4">Fatura</th>
<th className="p-4">Valor</th>
<th className="p-4 text-center">Ações</th>
<th className="p-4">Estado</th>
<th className="p-4 text-right">Valor</th>
</tr>
</thead>
<tbody>
{faturas.filter(f => f.status === 'Em Validação').map(fatura => (
{faturas.filter(f => f.status === 'Pago').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>
@@ -1662,32 +1711,18 @@
</td>
<td className="p-4 text-slate-600 dark:text-slate-400">
<p className="text-sm">{fatura.categoria}</p>
<p className="text-xs">Vence: {fatura.dataVencimento}</p>
<p className="text-xs">Venceu a: {fatura.dataVencimento}</p>
</td>
<td className="p-4 font-bold text-slate-800 dark:text-slate-200">{Number(fatura.valor).toFixed(2)}</td>
<td className="p-4">
<div className="flex justify-center gap-2">
<button onClick={() => {
if(window.confirm('Aprovar o pagamento desta fatura?')) {
handleApproveFatura(fatura);
}
}} className="p-2 bg-green-100 text-green-600 rounded hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400" title="Aprovar Pagamento">
<CheckCircle size={18} />
</button>
<button onClick={() => {
if(window.confirm('Rejeitar este pagamento?')) {
set(ref(db, `faturas/${fatura.id}/status`), 'Pendente');
showNotification('Pagamento rejeitado.', 'warning');
}
}} className="p-2 bg-red-100 text-red-600 rounded hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400" title="Rejeitar Pagamento">
<X size={18} />
</button>
<td className="p-4 text-slate-600 dark:text-slate-400">
<div className="inline-flex items-center gap-1.5 px-2 py-1 rounded bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 text-xs font-medium">
<CheckCircle size={14} /> Pago
</div>
</td>
<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 === 'Em Validação').length === 0 && (
<tr><td colSpan="4" className="p-8 text-center text-slate-500">Nenhum pagamento pendente de aprovação.</td></tr>
{faturas.filter(f => f.status === 'Pago').length === 0 && (
<tr><td colSpan="4" className="p-8 text-center text-slate-500">Nenhum pagamento concluído encontrado.</td></tr>
)}
</tbody>
</table>
@@ -1915,10 +1950,6 @@
>
Pagar
</button>
) : fatura.status === 'Em Validação' ? (
<span className="text-orange-500 text-xs font-bold flex items-center justify-center gap-1">
<Clock size={14} /> Em Validação
</span>
) : (
<span className="text-slate-400 text-xs font-bold flex items-center justify-center gap-1">
<CheckCircle size={14} /> Pago
@@ -1971,12 +2002,32 @@
<h3 className="font-bold text-lg text-slate-800 dark:text-white">Diário Financeiro</h3>
<span className="text-xs bg-slate-100 dark:bg-dark-bg text-slate-600 dark:text-slate-400 px-2 py-1 rounded-full">{finances.length} movimentos</span>
</div>
<button
onClick={() => handleOpenModal('finance')}
className="bg-slate-900 dark:bg-slate-700 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-slate-800 dark:hover:bg-slate-600 flex items-center gap-2 shadow-lg hover:shadow-xl transition-all"
>
<Plus size={18} /> Novo Registo
</button>
<div className="flex items-center gap-3">
{finances.length === 0 && (
<button
onClick={async () => {
try {
for (const item of INITIAL_FINANCES) {
await set(push(ref(db, 'financas')), item);
}
showNotification("Dados de exemplo restaurados com sucesso!", "success");
} catch (error) {
console.error("Erro ao restaurar:", error);
showNotification("Erro ao restaurar.", "error");
}
}}
className="bg-orange-500 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-orange-600 transition-colors shadow-sm flex items-center gap-2"
>
<Wrench size={16} /> Restaurar Base de Dados
</button>
)}
<button
onClick={() => handleOpenModal('finance')}
className="bg-slate-900 dark:bg-slate-700 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-slate-800 dark:hover:bg-slate-600 flex items-center gap-2 shadow-lg hover:shadow-xl transition-all"
>
<Plus size={18} /> Novo Registo
</button>
</div>
</div>
<div className="overflow-auto flex-1">
<table className="w-full text-sm text-left">
@@ -2350,7 +2401,11 @@
}
const root = createRoot(document.getElementById('root'));
root.render(<App />);
root.render(
<ErrorBoundary>
<App />
</ErrorBoundary>
);
</script>
<!-- Firebase configs moved to top in React Module -->
</body>