ajustes
This commit is contained in:
151
index.html
151
index.html
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user