diff --git a/.firebase/hosting..cache b/.firebase/hosting..cache
index edcc9b9..457f88a 100644
--- a/.firebase/hosting..cache
+++ b/.firebase/hosting..cache
@@ -1,28 +1,32 @@
test_nif.js,1777390754764,b50e0b4fe7732186ea056d183a0f2bbe7026cf79d384ce66391f746fe6969af6
-sw.js,1770114976862,b003899c6761f1320628a7e6048429714950195393f134df16159399586d3ca4
+temp_script.jsx,1778062694175,e61487a0533f953dd5040c3f8bfba051119ee86ea855d2baf353bbe20f7abd86
+sw.js,1778065253229,20540da27a8005d1e7a54a576607c687217feb037aedb26e5d8dab53d69deb34
style.css,1770114976862,4c2e2686b637f6f2f060298dfbabf690219284ff4c5c027711c5b443dde07332
script.js,1776937115126,4b08e5f41663ef287d352039798448e36a3c52c60014d58f7ed31471dab4066d
manifest.json,1770114976862,eb6e5b596d2a562026e361e5ee5bd1f4c3fc94a5e3b8cfc9d8761c6d21b2b991
+index.html,1778067423164,0896158ce7fa3228f5ba10a60ab661d8dc55b4bb492c835399ce9626c8f40318
firebase.js,1776936516051,37feb64e428313ec44afdabc4a2a348f29aae1ce0893c8099205750b1b5faf87
firebase,1777902366124,9d8f53c2037285ddb56fad26e9a581980370cec0dae5bfae0e91e5a87e8b96b2
-RELATORIO_TECNICO.md,1777995323646,3219fcffa736181cafc9e9b13f0068017db9fdb7458f644973090e6b7b4b66e5
-README.md,1777387819540,1b62e096bfae58e52be118bb6e3ed79117a3c919af031d924821394e605dece4
-.vscode/settings.json,1776788233398,a44051331ea8dca3e97ad536007d1fb294c28038b178012e6fa9dc481db05ce9
-.git/index,1777903218134,26e327830d8918bcfe11e89809b61d4101f5a1860f26fd5d958f736719e1e629
+RELATORIO_TECNICO.md,1778058174796,fad35f12b1f2d062f72e7a448bb643fd3cfdd24423eaacc14cbbb20172ead7be
+README.md,1778057411525,cc32fa073114e014f06988652ebe4c6af9ba3dbf913b9f25d38a16ce3d700605
+.vscode/settings.json,1778056161665,3a247752ccf28f259e2e604bf44311ab91b6db3864b120e08f2823951d1c55d8
+.git/index,1777997762141,9c98b1a4efc883a6a6bb5d798da2aa83b7b0e07655aec8c43f71ba041de76b86
.git/description,1773160274654,3cdc7b6a29de07f63b76d16b9911d93468000346945f759d4f6456660b5c113b
.git/config,1773914677241,cb4cd1c7c28ac13d2dccf79f667245402cf551c998ba1f6b58abc28f3ae11e7f
.git/ORIG_HEAD,1777392979044,2612c449de4f930bfb197ddb13780ca77cbb7bf6db7e91493d412b634ddcdebd
.git/HEAD,1773160809135,a39dc51e21d1523cdef2091e7c7ab30a33ad42a7cd5da1f45139746e5c24b667
-.git/COMMIT_EDITMSG,1777903218135,bf4bd4dfbb9f6c9351b2313e5df24a395e7a43208ff8922572237c166422cbcf
-.git/refs/remotes/origin/main,1777903229622,30437e7b9aa4378a80a4ab39abb8a4925238b5ab69719fac495bdb8f4ee17616
+.git/COMMIT_EDITMSG,1777997762142,47e4b83fa796b7965feed27fd3d7017275b18a8518ff0cbc20bf12dcc0ff7e36
+.git/refs/remotes/origin/main,1777997762539,07acf1e52d80759a7a23b8c54921216d19f042b468bed7e35eecff2180712b24
.git/refs/remotes/origin/HEAD,1773830493701,0f5d56efe56c5dcabb387d965aad58d0f60a3b7485cb9b04bef04b93bebf911e
-.git/refs/heads/main,1777903218136,30437e7b9aa4378a80a4ab39abb8a4925238b5ab69719fac495bdb8f4ee17616
+.git/refs/heads/main,1777997762143,07acf1e52d80759a7a23b8c54921216d19f042b468bed7e35eecff2180712b24
.git/objects/fd/3d3838a9118dc446e6ab65d38a5ce1747fd0d6,1777393032091,b7fcc251a3edcfc6eb1d7a0ea70a10d16f297520eb5e95c822ab5f754da8dc4a
.git/objects/f6/73a71b7a275609030462c0278586d61e5f3a00,1773830457776,6ef355c279049956337049bd863d18f205f75ec1bcaee8c82bf26a7d2a65af72
.git/objects/f4/577dec9e03ee9efa3af7ee05d60576b07f8d99,1773161014724,c78b5a6ceff9eb62984db81b7041efbdcac774d45a96d7536ede20a9a6ba10c5
.git/objects/f3/7d7211c51f051db56b0e67a1bfca55f649e1e5,1773161195360,a0961540cd8d800dd424b0eb7d503bb35012ad90477c1ec10b9e4b34e1102027
+.git/objects/ed/cc9b9ffb9c172f4579f71d3e10414f58ea26d0,1777997762125,439ee4c3ff19456eafa85052089df8c4ea4c00c1c293fd4c639377d8d125ebb9
.git/objects/ea/2dee2f517449595d633889a410cc2aefa2b513,1776937200147,9392a6f5dd0687166fd9b60a4f28176604046d293196fe53038e9d2e456d5336
.git/objects/e8/5b891f05021af7f78059851bac361ba110e459,1777903218123,c0684017afd5d4cb4234a4191773d088b5ae747e88b477cabcd08fff53e24450
+.git/objects/e7/1629b1f461748a0642a397d9aa94d4425d3a6a,1777997762126,7511a853da08dcde2f169bc9afe5b92eb9028dc27052c95a436b62368897e4ad
.git/objects/e0/6cdd4ddfacadcef9577916c84406559b985623,1773161195362,d89c95c83f040e788bd182a67bc3cbb9afdf7d3740a388cc4d43d2514b80e556
.git/objects/da/444cce2c8426f321e774e9bc8134642bae50ae,1773830457778,293cb0fb7b39d4ffbece90d8f57bcd730c29d754a284c7d83436a489661e507e
.git/objects/d3/f58873ab4f70d24d92349c7e85c7df5c7bb7c2,1776937200165,ca8deaee745fd2e87b4804183acc8c484a36b88db729b2c4cb52f17f99ffb747
@@ -37,6 +41,7 @@ README.md,1777387819540,1b62e096bfae58e52be118bb6e3ed79117a3c919af031d924821394e
.git/objects/94/5d8710c7134fdf4feaa701af8d88bda61db300,1776957069564,b68f1087c81f6a22ae676c795169f34674865c85b4e9631b82db50a73cb190d5
.git/objects/92/d9d123244957aa1b1dd1a6f54b9899ec28def8,1773161195399,3f3e3c1f4e739595d33d89aef665c3933ec1af7914020cf0ff6de3e3d41d0109
.git/objects/8e/7980160dd6e37a0edf181cb022b86334e205fd,1777393032077,921856f2b98e34016c387ef1f73025bba7ed846c64d8017ec6d2e6e0e9f7c586
+.git/objects/8d/053f517afe910c5933a43eca4e7dad2fe5edf1,1777997762122,e3802ac195f07bda67d59b5754f2254282916e9d5277388af9751fc1cb511723
.git/objects/8c/b6ffc314647094561a18f0af805412260abc3b,1776326595546,a256a28d0e28ac76431ae5406923461a7b4fd7aaac40f5dae947377abc2ca830
.git/objects/84/2dd08f73738644fe58eecb5409d5ebd1544efb,1776326595582,b5b51d6c09e2f4e9767489040755ab4e1c4cdce30f46567e4391e0e228f04395
.git/objects/7f/0d5ab4d6ff0318cbd6a9c3f8eb57aaac4634a4,1777542176251,16d9b4319d629d55eb92c57e1070cca1671b642d2c780895fc600570fcb5b895
@@ -56,7 +61,10 @@ README.md,1777387819540,1b62e096bfae58e52be118bb6e3ed79117a3c919af031d924821394e
.git/objects/40/4bcf86370b3aa54e5c09e50a7be47241fe63ee,1773830230299,f9bae290124afebf31f114d71dc73d407a04a248fff87a729abfa68e807e6cf9
.git/objects/3b/cafc02b8f3692fbb5b6d93debe13bb50563c2a,1776937200150,70a72e7716314bf0c914dd3d802a5a0ba4c93a23c179aaa86fb8c660f039e344
.git/objects/38/465e5ae301b3ddefabfe22b188c4fee52182c0,1773161195398,dbcf76217184aff41336a8f6530dc63b073bb4235b0fec701dfa33df27a0b402
+.git/objects/33/28ec86365d3f1a608e8dd533291ff447c97d00,1777997762141,34991e7e8382ce6d32b8a5a1ee47d64613523696d9a8b5ae59cbf5375155559d
.git/objects/32/1c6b6ef5843bf86598659bb85cf0ef3d63ccc8,1773161000773,7781172d118bad5f4282ecc246a2e432d07e682ebb5652c325a8d26d272f804c
+.git/objects/31/fb5e7eceb602bd43982765eed9f3848c86990a,1777997762121,2ad3979c7c6f61f78c82c768b402e59a806f9b3fea4acb9065548670b7b8a235
+.git/objects/30/9efec640a08f35557a83afabc2329354be495b,1777997762140,a5f9548d56a5a654f7c326a001e360dd12bf0ac74382d02f98023faa30b1484e
.git/objects/2d/a32767f43e7735bca583bf1aa5c7436ae485fe,1773830457795,f1cc7925dc66329b987ae56725551624b57040a581eedabe08e1eb66dbe6e6c2
.git/objects/2d/085bcbe50700bbcff79f9186c9c317d9bb1ab4,1773830457780,b8a511b75f969777c2fb6d5e3e0538da8888a4817ae34d0e3192c7fe2695538a
.git/objects/2c/fea43d887fc1e25adba99d01fc094e26bb25ae,1777903218123,3ff6da2a0ec8c954f94eeb6423bc392ec995d9b56198c375ad2ac417943dc43c
@@ -64,6 +72,7 @@ README.md,1777387819540,1b62e096bfae58e52be118bb6e3ed79117a3c919af031d924821394e
.git/objects/2b/d5891243d72a793a41ea51994856446a350f55,1777387356904,0547ff6336463e58b4d266d7a08388b2ee53c501b37d95bf19846f896a3bd1a2
.git/objects/2a/f13184106a656f511871672a98dca04e58f04b,1776937200165,207a73dfdc380c9ff77147f77f094d28c1e6471f74fc37175c9cc6dde8ff2b3b
.git/objects/2a/a7aa1819e7c69a73c81dadbf3643a3aec6fed5,1776183305313,9e31ebefd7d25152cd322bd673d587b1e1367ae75e01aed02e86d6b58f5a430a
+.git/objects/27/85c3386d34cce3cafd66e5892675c61548c3df,1777997762124,9362c43fef34a74a522bf592f011f778c530bba1edbf624926f84d7f9bf54183
.git/objects/25/b0e88e2c1a6db6b52948a4d2fc649e06de8cb2,1776957069549,67160c871e47bc652ab9b8a11b65efe4a559f4dc7a84c16e9aeed10cd44d41b0
.git/objects/25/5c1a39f46ad00812b25cf59858aabb19ebfa18,1776957069566,c328ecb00d90ba099ec4534307c0f3177478656f0fc2d21b4e02f6e743cf1773
.git/objects/23/cec1dc9936a6d9be11078377c6a7685b26373f,1776183305295,d709b165a1199d66da18ba51afe1471aa7b068d4066284bf28d333f09e0c9670
@@ -73,10 +82,11 @@ README.md,1777387819540,1b62e096bfae58e52be118bb6e3ed79117a3c919af031d924821394e
.git/objects/0f/e52b062580722aedaf1e72d0873dfcd52ae1d0,1777393032092,cda51340d22051dc215c44006c12ac911bfac5c0a0e9535fb2f5acb8fd2a62e5
.git/objects/0d/982210a52ba6078db202a8b2ac2d04e8d4ac41,1776326595581,c4c80ce646b15b1aa2e9811149739253f98987c4eb84bdf21d21410d902b2ee4
.git/objects/0b/fc5efae1d163add058f1940feea6649f5da1b1,1777393032074,f19ecb9e3799eb6cd2f62e145b429f8c7c48fe8ffda704c49d6d925e9818c000
-.git/logs/HEAD,1777903218136,0e77927c7f96859d395e49b6ea9f8d7f54c6e6c60d00fef26fbd90459c5fe112
-.git/logs/refs/remotes/origin/main,1777903229624,d2f8ee46f879dca77a59471f0657aca5ce92f42b34044ae02702a1f788e79305
+.git/objects/00/0c1cd721b99d14281c3724f5488b040c152515,1777997762142,7d131dd9c4c33d6e017428809d3b927f369ee8fa38c7d6ae79d01bc10561e22b
+.git/logs/HEAD,1777997762143,f1258009669fe6da86a2aee96326d56f7d477b661aad447ff3bdaabbf9701c91
+.git/logs/refs/remotes/origin/main,1777997762540,e34bfe04d74b4940da23b0edef8c5d374a152af44181fc4904f4f67a9711c253
.git/logs/refs/remotes/origin/HEAD,1773830493702,1eba2cff5035849e216a15d3b6013593fa5ef345a8d76bb2881d83b3cb247576
-.git/logs/refs/heads/main,1777903218136,0e77927c7f96859d395e49b6ea9f8d7f54c6e6c60d00fef26fbd90459c5fe112
+.git/logs/refs/heads/main,1777997762143,f1258009669fe6da86a2aee96326d56f7d477b661aad447ff3bdaabbf9701c91
.git/info/exclude,1773160274653,a362e375cc3330f10d115cfeb0f90a325219d80a764d57e2c4873f78d1d0b4f5
.git/hooks/update.sample,1773160274656,2b0a4f42fa30a128b46ad80e89c1f73b89d58b8abb9e92aee1c35625baccb584
.git/hooks/sendemail-validate.sample,1773160274654,4d0768bc11017be6b99d4bb4d34b4c8b2fd7ae8a93d42727591afb6737577db2
@@ -92,5 +102,4 @@ README.md,1777387819540,1b62e096bfae58e52be118bb6e3ed79117a3c919af031d924821394e
.git/hooks/fsmonitor-watchman.sample,1773160274655,d366d691e33458260d77c44be36050a3faf0aa12760955cc8ca85ee88389c400
.git/hooks/commit-msg.sample,1773160274654,4df962ba3955944bec38b211351c73f083d7b0e5360a5d3d76a49548e7314f9e
.git/hooks/applypatch-msg.sample,1773160274655,91b94f5feaf0e4d2e6e7808a9188384a4300adf024fa24c48547ee87c64d6558
-.git/FETCH_HEAD,1777997077083,143159fa1a2fe0eca23473e5ac2923426b9a541be39e3eda8059a80f58991b05
-index.html,1777996984662,b1d6a4fbac086eece329ec7f5243c5d834ee206096ecd3730ae7f35817b41c1b
+.git/FETCH_HEAD,1778067520987,8dd09ee3bc8bc73b170ad713c1b297d159c64332b8fc924178df7c320512fa9a
diff --git a/.firebaserc b/.firebaserc
index 31fb5e7..751bb22 100644
--- a/.firebaserc
+++ b/.firebaserc
@@ -6,7 +6,7 @@
"condomaster-pro-ed9af": {
"hosting": {
"condomaster": [
- "condomaster-pro-web"
+ "mycondominium-web"
]
}
}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index a0de46f..85245c9 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,3 +1,3 @@
{
- "liveServer.settings.port": 5504
+ "liveServer.settings.port": 5505
}
\ No newline at end of file
diff --git a/README.md b/README.md
index 0bfc5ef..110a88a 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
-# CondoMasterResults
+# MyCondominium
Uma plataforma moderna de gestão de condomínios focada em transparência, comunicação em tempo real e facilidade de uso, tanto para moradores como para a administração.
## 🎯 Sobre o Projeto
-O **CondoMasterResults** é uma *Single Page Application* concebida para digitalizar a gestão do dia a dia num condomínio. Permite aos moradores consultar despesas, reservar espaços comuns e reportar ocorrências, enquanto oferece aos administradores um painel de controlo completo sobre as finanças e os utilizadores.
+O **MyCondominium** é uma *Single Page Application* concebida para digitalizar a gestão do dia a dia num condomínio. Permite aos moradores consultar despesas, reservar espaços comuns e reportar ocorrências, enquanto oferece aos administradores um painel de controlo completo sobre as finanças e os utilizadores.
## 🚀 Funcionalidades Chave
diff --git a/RELATORIO_TECNICO.md b/RELATORIO_TECNICO.md
index e71629b..f390a93 100644
--- a/RELATORIO_TECNICO.md
+++ b/RELATORIO_TECNICO.md
@@ -1,7 +1,7 @@
-# Relatório Técnico - CondoMaster Pro
+# Relatório Técnico - MyCondominium
## 1. Visão Geral do Projeto
-O **CondoMaster Pro** é uma aplicação web moderna dedicada à gestão de condomínios. Permite uma interação contínua entre administradores e moradores, fornecendo ferramentas para gestão de quotas, ocorrências de manutenção, reservas de espaços comuns, faturação e comunicação em tempo real (chat privado, global e em grupo).
+O **MyCondominium** é uma aplicação web moderna dedicada à gestão de condomínios. Permite uma interação contínua entre administradores e moradores, fornecendo ferramentas para gestão de quotas, ocorrências de manutenção, reservas de espaços comuns, faturação e comunicação em tempo real (chat privado, global e em grupo).
---
@@ -46,7 +46,7 @@ Gere os logins e registos. Garante que apenas utilizadores validados acedem à p
A aplicação está hospedada no **Firebase Hosting**, uma rede de distribuição global (CDN) ultrarrápida.
-* **URL Oficial Atual:** [https://condomaster-pro-web.web.app](https://condomaster-pro-web.web.app)
+* **URL Oficial Atual:** [https://mycondominium-web.web.app](https://mycondominium-web.web.app)
* **Configuração de Segurança e Performance (`firebase.json`):**
* **Cache-Control:** Os recursos estáticos têm instruções para ficar na cache dos browsers durante 1 ano, permitindo que a app carregue quase instantaneamente após a primeira visita.
* **Rewrites (SPA):** Qualquer URL acedido é reencaminhado para o `index.html`. Isto previne que ocorra um Erro 404 se um utilizador fizer "Refresh" no browser.
diff --git a/firebase.json b/firebase.json
index 8d053f5..1edf059 100644
--- a/firebase.json
+++ b/firebase.json
@@ -14,6 +14,15 @@
}
],
"headers": [
+ {
+ "source": "**/*.html",
+ "headers": [
+ {
+ "key": "Cache-Control",
+ "value": "no-cache, no-store, must-revalidate"
+ }
+ ]
+ },
{
"source": "**/*.@(js|css|png|jpg|jpeg|gif|svg|woff|woff2)",
"headers": [
diff --git a/index.html b/index.html
index 2785c33..28dcc7b 100644
--- a/index.html
+++ b/index.html
@@ -5,7 +5,7 @@
-
CondoMaster Pro
+ MyCondominium
@@ -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 (
+
+
Algo correu mal (Erro na Aplicação)
+
+ {this.state.error && this.state.error.toString()}
+
+ {this.state.errorInfo && this.state.errorInfo.componentStack}
+
+
window.location.reload()} style={{ marginTop: '20px', padding: '10px 20px', backgroundColor: '#dc2626', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}>Recarregar Página
+
+ );
+ }
+ return this.props.children;
+ }
+ }
+
const auth = getAuth(app);
const db = getDatabase(app);
@@ -326,7 +357,7 @@
- CondoMasterPro
+ MyCondominium
Portal de Gestão
@@ -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 @@
-
+
@@ -1422,7 +1471,7 @@
-
CondoMasterPro
+
MyCondominium
Portal de Gestão
@@ -1639,8 +1688,8 @@
{activeTab === 'approvals' && userRole === 'admin' && (
-
Aprovações de Pagamentos
-
Valide ou rejeite pagamentos de faturas enviados pelos condóminos.
+
Pagamentos Concluídos
+
Consulte o histórico de todos os pagamentos concluídos pelos condóminos.
@@ -1649,12 +1698,12 @@
Morador
Fatura
- Valor
- Ações
+ Estado
+ Valor
- {faturas.filter(f => f.status === 'Em Validação').map(fatura => (
+ {faturas.filter(f => f.status === 'Pago').map(fatura => (
{fatura.nomeMorador}
@@ -1662,32 +1711,18 @@
{fatura.categoria}
- Vence: {fatura.dataVencimento}
+ Venceu a: {fatura.dataVencimento}
- {Number(fatura.valor).toFixed(2)}€
-
-
-
{
- 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">
-
-
-
{
- 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">
-
-
+
+
+ Pago
+
{Number(fatura.valor).toFixed(2)}€
))}
- {faturas.filter(f => f.status === 'Em Validação').length === 0 && (
- Nenhum pagamento pendente de aprovação.
+ {faturas.filter(f => f.status === 'Pago').length === 0 && (
+ Nenhum pagamento concluído encontrado.
)}
@@ -1915,10 +1950,6 @@
>
Pagar
- ) : fatura.status === 'Em Validação' ? (
-
- Em Validação
-
) : (
Pago
@@ -1971,12 +2002,32 @@
Diário Financeiro
{finances.length} movimentos
-
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"
- >
- Novo Registo
-
+
+ {finances.length === 0 && (
+
{
+ 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"
+ >
+ Restaurar Base de Dados
+
+ )}
+
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"
+ >
+ Novo Registo
+
+
@@ -2350,7 +2401,11 @@
}
const root = createRoot(document.getElementById('root'));
- root.render( );
+ root.render(
+
+
+
+ );
+
+
+ import React, { useState, useEffect, useRef } from 'react';
+ import { createRoot } from 'react-dom/client';
+ import {
+ Building2, Users, Wallet, Wrench, Bell, Search, Plus, Menu, X,
+ TrendingUp, TrendingDown, CheckCircle, AlertCircle, Clock, LogOut,
+ Edit2, Trash2, Save, Filter, MoreVertical, FileText,
+ 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';
+
+ const auth = getAuth(app);
+ const db = getDatabase(app);
+
+ const INITIAL_RESIDENTS = [
+ { id: 1, unit: '1º Esq', name: 'Ana Silva', contact: '912 345 678', email: 'ana.silva@email.com', status: 'Pago', pending: 0, role: 'morador' },
+ { id: 2, unit: '1º Dto', name: 'Carlos Santos', contact: '965 432 109', email: 'carlos.s@email.com', 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: 'Pago', pending: 0, role: 'morador' },
+ { id: 4, unit: '2º Dto', name: '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: 'Sofia Costa', contact: '922 334 455', email: 'sofia.c@email.com', 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' },
+ ];
+
+ 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: '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' },
+ ];
+
+ const INITIAL_BOOKINGS = [
+ { id: 1, facility: 'hall', facilityName: 'Salão de Festas', date: '2023-10-25', time: '14:00 - 20:00', resident: 'Ana Silva', status: 'Confirmado', cost: 50 },
+ { id: 2, facility: 'gym', facilityName: 'Ginásio', date: '2023-10-20', time: '09:00 - 10:00', resident: 'Carlos Santos', 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: '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 },
+ ];
+
+ // --- VALIDAÇÕES OFICIAIS ---
+ function validarNIF(nif) {
+ nif = String(nif).replace(/\s+/g, '');
+ return /^\d{9}$/.test(nif);
+ }
+
+ function validarDocumento(doc) {
+ let docStr = doc.replace(/[\s-]/g, '').toUpperCase();
+
+ // Muitos utilizadores inserem apenas os 8 dígitos do NIC, o que não tem check-digit na própria string
+ if (/^\d{8}$/.test(docStr)) {
+ return true;
+ }
+
+ if (/^\d{9}$/.test(docStr)) {
+ let checkDigitValue = parseInt(docStr.charAt(docStr.length - 1), 10);
+ let soma = 0;
+ for (let i = 0; i < docStr.length - 1; i++) {
+ soma += parseInt(docStr.charAt(i), 10) * (docStr.length - i);
+ }
+ let resto = soma % 11;
+ let expectedDigit = (resto === 0 || resto === 1) ? 0 : (11 - resto);
+ return expectedDigit === checkDigitValue;
+ }
+ if (docStr.length === 12) {
+ let sum = 0;
+ let isSecond = false;
+ for (let i = docStr.length - 1; i >= 0; i--) {
+ let charCode = docStr.charCodeAt(i);
+ let val = 0;
+ if (charCode >= 48 && charCode <= 57) val = charCode - 48;
+ else if (charCode >= 65 && charCode <= 90) val = charCode - 55;
+ else return false;
+
+ if (isSecond) {
+ val *= 2;
+ if (val >= 36) val -= 36;
+ }
+ sum += val;
+ isSecond = !isSecond;
+ }
+ return (sum % 36) === 0;
+ }
+ return false;
+ }
+
+ const Modal = ({ isOpen, onClose, title, children }) => {
+ if (!isOpen) return null;
+ return (
+
+
+
+
{title}
+
+
+
+
+
{children}
+
+
+ );
+ };
+
+ const InputGroup = ({ label, name, type = 'text', value, onChange, placeholder, required = false, options = null, disabled = false }) => (
+
+
+ {label} {required && * }
+
+ {options ? (
+
+ {options.map(opt => (
+ {opt.label}
+ ))}
+
+ ) : (
+
+ )}
+
+ );
+
+ const SidebarItem = ({ icon: Icon, label, active, onClick }) => (
+
+
+ {label}
+
+ );
+
+ const Card = ({ title, value, icon: Icon, trend, trendValue, color, subtitle }) => (
+
+
+
+
+ {trend === 'up' ? (
+
+ {trendValue}
+
+ ) : trend === 'down' ? (
+
+ {trendValue}
+
+ ) : (
+
+ —
+
+ )}
+
+
{subtitle || 'vs. mês passado'}
+
+
+ );
+
+ const Badge = ({ status }) => {
+ const styles = {
+ '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',
+ 'Resolvido': 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800',
+ 'Receita': 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800',
+ 'Confirmado': 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800',
+ 'Pendente': 'bg-yellow-100 text-yellow-700 border-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400 dark:border-yellow-800',
+ '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',
+ 'Em Progresso': 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-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',
+ 'Atrasado': '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',
+ 'Alta': 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800',
+ 'Novo': 'bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-900/30 dark:text-purple-400 dark:border-purple-800',
+ 'Baixa': 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800',
+ };
+
+ return (
+
+ {status}
+
+ );
+ };
+
+ const LoginView = ({ onLogin }) => {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ const success = await onLogin(email, password);
+ if (!success) {
+ setError('Email ou Palavra-passe incorreta');
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
MyCondominium
+
Portal de Gestão
+
+
+
+
+
+ );
+ };
+
+ function App() {
+ const [activeTab, setActiveTab] = useState('dashboard');
+ const [isSidebarOpen, setSidebarOpen] = useState(false);
+ const [searchQuery, setSearchQuery] = useState('');
+ const [theme, setTheme] = useState('system');
+
+ const [isAuthenticated, setIsAuthenticated] = useState(() => {
+ return sessionStorage.getItem('condo_auth') === 'true';
+ });
+ const [userRole, setUserRole] = useState(() => {
+ return sessionStorage.getItem('condo_role') || 'morador';
+ });
+ const [currentUserName, setCurrentUserName] = useState(() => {
+ return sessionStorage.getItem('condo_user_name') || 'Utilizador';
+ });
+ const [currentUserId, setCurrentUserId] = useState(() => {
+ return sessionStorage.getItem('condo_user_id') || '0';
+ });
+ const [userStatus, setUserStatus] = useState(() => {
+ return sessionStorage.getItem('condo_user_status') || 'aprovado';
+ });
+
+
+ const handleLogin = async (email, password) => {
+ try {
+ const userCredential = await signInWithEmailAndPassword(auth, email, password);
+ let role = 'morador';
+ let userName = 'Utilizador';
+ let userId = userCredential.user.uid;
+ let status = 'aprovado';
+
+ if (email.toLowerCase().includes('admin')) {
+ role = 'admin';
+ userName = 'Administração';
+ } else {
+ const residentUser = residents.find(r => r.id === userId || (r.email && r.email.toLowerCase() === email.toLowerCase()));
+ if (residentUser) {
+ role = residentUser.role || 'morador';
+ userName = residentUser.name + (residentUser.unit && residentUser.unit !== 'Pendente' ? ` (${residentUser.unit})` : '');
+ userId = residentUser.id || userId;
+ status = residentUser.status || 'aprovado';
+ } else {
+ status = 'pendente'; // Fallback if missing
+ }
+ }
+ sessionStorage.setItem('condo_auth', 'true');
+ sessionStorage.setItem('condo_role', role);
+ sessionStorage.setItem('condo_user_name', userName);
+ sessionStorage.setItem('condo_user_id', userId);
+ sessionStorage.setItem('condo_user_status', status);
+ setIsAuthenticated(true);
+ setUserRole(role);
+ setCurrentUserName(userName);
+ setCurrentUserId(userId);
+ setUserStatus(status);
+ return true;
+ } catch (error) {
+ console.log("Firebase Auth falhou, a tentar conta local...", error);
+ let role = null;
+ let userName = 'Utilizador';
+ let userId = 'local_' + Date.now();
+
+ let status = 'aprovado';
+
+ if (email === 'administradores@gmail.com' && password === 'admin123') {
+ role = 'admin';
+ userName = 'Administração';
+ userId = 'admin_001';
+ } else {
+ const residentUser = residents.find(r => r.email && r.email.toLowerCase() === email.toLowerCase());
+ if (residentUser && (password === residentUser.contact || password === '1234')) {
+ role = residentUser.role || 'morador';
+ userName = residentUser.name + (residentUser.unit && residentUser.unit !== 'Pendente' ? ` (${residentUser.unit})` : '');
+ userId = residentUser.id || userId;
+ status = residentUser.status || 'aprovado';
+ }
+ }
+
+ if (role) {
+ sessionStorage.setItem('condo_auth', 'true');
+ sessionStorage.setItem('condo_role', role);
+ sessionStorage.setItem('condo_user_name', userName);
+ sessionStorage.setItem('condo_user_id', userId);
+ sessionStorage.setItem('condo_user_status', status);
+ setIsAuthenticated(true);
+ setUserRole(role);
+ setCurrentUserName(userName);
+ setCurrentUserId(userId);
+ setUserStatus(status);
+ return true;
+ }
+ return false;
+ }
+ };
+
+ const handleLogout = () => {
+ if (window.confirm('Tem a certeza que deseja terminar sessão?')) {
+ sessionStorage.removeItem('condo_auth');
+ sessionStorage.removeItem('condo_role');
+ sessionStorage.removeItem('condo_user_name');
+ sessionStorage.removeItem('condo_user_id');
+ sessionStorage.removeItem('condo_user_status');
+ setIsAuthenticated(false);
+ setUserRole(null);
+ setCurrentUserName('Utilizador');
+ setCurrentUserId('0');
+ setUserStatus('aprovado');
+ setActiveTab('dashboard');
+ }
+ };
+
+ const [residents, setResidents] = useState([]);
+ const [finances, setFinances] = useState([]);
+ const [issues, setIssues] = useState([]);
+ const [bookings, setBookings] = useState([]);
+ const [invoices, setInvoices] = useState([]);
+ const [faturas, setFaturas] = useState([]);
+ const [messages, setMessages] = useState([]);
+ const [newMessageText, setNewMessageText] = useState('');
+ const [activeChat, setActiveChat] = useState({ type: 'global', id: 'global', name: 'Fórum do Condomínio' });
+ const [chatGroups, setChatGroups] = useState([]);
+ const [isCreateGroupModalOpen, setIsCreateGroupModalOpen] = useState(false);
+ const [newGroupName, setNewGroupName] = useState('');
+ const [newGroupMembers, setNewGroupMembers] = useState([]);
+
+ 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]) => ({ id, ...val }));
+ if (sortFunc) parsed = parsed.sort(sortFunc);
+ setter(parsed);
+ } else {
+ setter([]);
+ }
+ }, (error) => console.error(`Erro ao carregar ${path}:`, error));
+ };
+
+ 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 unsubGroups = loadData('grupos_chat', setChatGroups);
+
+ return () => {
+ unsubResidents();
+ unsubFinances();
+ unsubIssues();
+ unsubBookings();
+ unsubInvoices();
+ unsubFaturas();
+ unsubGroups();
+ };
+ }, []);
+
+ useEffect(() => {
+ if (!isAuthenticated || !currentUserId) {
+ setNotificationsList([]);
+ return;
+ }
+
+ const targetFolder = userRole === 'admin' ? 'admin' : currentUserId;
+ const path = `notificacoes/${targetFolder}`;
+
+ const unsub = onValue(ref(db, path), (snapshot) => {
+ const data = snapshot.val();
+ if (data) {
+ let parsed = Object.entries(data).map(([id, val]) => ({ id, ...val }));
+ parsed = parsed.sort((a,b) => b.timestamp - a.timestamp);
+ setNotificationsList(parsed);
+ } else {
+ setNotificationsList([]);
+ }
+ }, (error) => console.error(`Erro ao carregar notificações:`, error));
+
+ return () => unsub();
+ }, [isAuthenticated, currentUserId, userRole]);
+
+ useEffect(() => {
+ let path = 'mural_mensagens';
+ if (activeChat.type === 'private') {
+ path = `mensagens_privadas/${[currentUserId, activeChat.id].sort().join('_')}`;
+ } else if (activeChat.type === 'group') {
+ path = `mensagens_grupo/${activeChat.id}`;
+ }
+
+ const unsub = onValue(ref(db, path), (snapshot) => {
+ const data = snapshot.val();
+ if (data) {
+ let parsed = Object.entries(data).map(([id, val]) => ({ id, ...val }));
+ parsed = parsed.sort((a,b) => a.timestamp - b.timestamp);
+ setMessages(parsed);
+ } else {
+ setMessages([]);
+ }
+ }, (error) => console.error(`Erro ao carregar mensagens de ${path}:`, error));
+
+ return () => unsub();
+ }, [activeChat, currentUserId]);
+ const [notificationsList, setNotificationsList] = useState([]);
+ const [isNotificationsOpen, setNotificationsOpen] = useState(false);
+
+ const [activeModal, setActiveModal] = useState(null);
+ const [editingItem, setEditingItem] = useState(null);
+
+ const [notification, setNotification] = useState(null);
+
+ const notificationRef = useRef(null);
+
+ const initialResidentForm = { unit: '', name: '', contact: '', email: '', 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 };
+ const initialFaturaForm = { moradorId: '', categoria: '', valor: '', dataVencimento: new Date().toISOString().split('T')[0] };
+
+ const [formData, setFormData] = useState({});
+
+ useEffect(() => {
+ const root = window.document.documentElement;
+ root.classList.remove('dark');
+
+ if (theme === 'dark') {
+ root.classList.add('dark');
+ } else if (theme === 'system') {
+ if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
+ root.classList.add('dark');
+ }
+ }
+ }, [theme]);
+
+ useEffect(() => {
+ if (notification) {
+ const timer = setTimeout(() => setNotification(null), 3000);
+ return () => clearTimeout(timer);
+ }
+ }, [notification]);
+
+ useEffect(() => {
+ function handleClickOutside(event) {
+ if (notificationRef.current && !notificationRef.current.contains(event.target)) {
+ setNotificationsOpen(false);
+ }
+ }
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ };
+ }, [notificationRef]);
+
+ const toggleSidebar = () => setSidebarOpen(!isSidebarOpen);
+
+ 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 !== 'Resolvido').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())
+ );
+
+ const sendSystemNotification = async (message, type = 'info', targetUserId = 'admin') => {
+ const newNotif = { timestamp: Date.now(), message, time: 'Agora', type, read: false };
+ 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);
+ } else {
+ await push(ref(db, `notificacoes/${targetUserId}`), newNotif);
+ }
+ };
+
+ const handleMarkAsRead = async (notifId) => {
+ const targetFolder = userRole === 'admin' ? 'admin' : currentUserId;
+ const notifRef = ref(db, `notificacoes/${targetFolder}/${notifId}`);
+ await update(notifRef, { read: true });
+ };
+
+ const showNotification = (message, type = 'success') => {
+ setNotification({ message, type });
+ };
+
+ const handleClearNotifications = async () => {
+ const targetFolder = userRole === 'admin' ? 'admin' : currentUserId;
+ await set(ref(db, `notificacoes/${targetFolder}`), null);
+ setNotificationsOpen(false);
+ };
+
+ const handleOpenModal = (type, item = null, defaultFacility = null) => {
+ setEditingItem(item);
+ setActiveModal(type);
+
+ if (type === 'resident') {
+ setFormData(item || initialResidentForm);
+ } else if (type === 'finance') {
+ setFormData(initialFinanceForm);
+ } else if (type === 'issue') {
+ setFormData(initialIssueForm);
+ } else if (type === 'emitir_fatura') {
+ setFormData(initialFaturaForm);
+ } else if (type === 'booking') {
+ const baseForm = initialBookingForm;
+ if (defaultFacility) baseForm.facility = defaultFacility;
+ setFormData(baseForm);
+ }
+ };
+
+ const handleCloseModal = () => {
+ setActiveModal(null);
+ setEditingItem(null);
+ setFormData({});
+ };
+
+ const handleInputChange = (e) => {
+ const { name, value } = e.target;
+ setFormData(prev => {
+ const newData = { ...prev, [name]: value };
+ if (name === 'facility' || name === 'time') {
+ let cost = 0;
+ if (newData.facility === 'hall') cost = 50;
+ if (newData.facility === 'park') cost = 10;
+ newData.cost = cost;
+ }
+ return newData;
+ });
+ };
+
+ const handleToggleRole = async (id) => {
+ try {
+ const resident = residents.find(r => r.id === id);
+ if (resident) {
+ const newRole = resident.role === 'admin' ? 'morador' : 'admin';
+ const residentRef = ref(db, `condominos/${id}`);
+ await set(residentRef, { ...resident, role: newRole });
+ showNotification('Permissões de utilizador atualizadas', 'success');
+ }
+ } catch (error) {
+ console.error("Erro ao atualizar permissão:", error);
+ showNotification("Erro ao atualizar permissão.", "error");
+ }
+ };
+
+ const handleSaveResident = async (e) => {
+ e.preventDefault();
+ try {
+ if (editingItem) {
+ const residentRef = ref(db, `condominos/${editingItem.id}`);
+ await set(residentRef, {
+ unit: formData.unit || '',
+ name: formData.name || '',
+ contact: formData.contact || '',
+ email: formData.email || '',
+ status: formData.status || 'Pago',
+ pending: Number(formData.pending) || 0,
+ role: formData.role || 'morador'
+ });
+ showNotification(`Condómino ${formData.name} atualizado`);
+ } else {
+ const residentsListRef = ref(db, 'condominos');
+ const newResidentRef = push(residentsListRef);
+ await set(newResidentRef, {
+ unit: formData.unit || '',
+ name: formData.name || '',
+ contact: formData.contact || '',
+ email: formData.email || '',
+ status: formData.status || 'Pago',
+ pending: Number(formData.pending) || 0,
+ role: formData.role || 'morador'
+ });
+ showNotification(`Novo condómino ${formData.name} adicionado`);
+ }
+ handleCloseModal();
+ } catch (error) {
+ console.error("Erro ao guardar no Firebase:", error);
+ showNotification("Erro ao guardar os dados.", "error");
+ }
+ };
+
+ const handleDeleteResident = async (id) => {
+ if (window.confirm('Tem a certeza que deseja eliminar este condómino?')) {
+ try {
+ const residentRef = ref(db, `condominos/${id}`);
+ await remove(residentRef);
+ showNotification('Condómino removido', 'error');
+ } catch (error) {
+ console.error("Erro ao eliminar no Firebase:", error);
+ showNotification("Erro ao eliminar.", "error");
+ }
+ }
+ };
+
+ const handleSaveFinance = async (e) => {
+ e.preventDefault();
+ if (!formData.amount || !formData.category || !formData.date) {
+ showNotification("Preencha todos os campos obrigatórios.", "error");
+ return;
+ }
+ try {
+ const amount = Number(formData.amount);
+ const newFinanceRef = push(ref(db, 'financas'));
+ await set(newFinanceRef, { ...formData, amount });
+
+ if (formData.type === 'expense') {
+ sendSystemNotification(`Nova despesa registada: ${formData.category} - ${amount.toFixed(2)}€`, 'warning', 'admin');
+ } else {
+ sendSystemNotification(`Nova receita registada: ${formData.category} - ${amount.toFixed(2)}€`, 'success', 'admin');
+ }
+
+ showNotification(`Movimento de ${amount}€ registado`);
+ handleCloseModal();
+ } catch (error) {
+ console.error("Erro ao guardar finanças:", error);
+ showNotification("Erro ao guardar movimento.", "error");
+ }
+ };
+
+ const handleSaveIssue = async (e) => {
+ e.preventDefault();
+ if (!formData.title || !formData.location) {
+ showNotification("Preencha todos os campos obrigatórios.", "error");
+ return;
+ }
+ try {
+ const newIssueRef = push(ref(db, 'manutencao'));
+ await set(newIssueRef, { ...formData });
+
+ sendSystemNotification(`Nova ocorrência reportada: ${formData.title} (${formData.location})`, 'warning', 'admin');
+ if (userRole !== 'admin') {
+ sendSystemNotification(`A sua ocorrência "${formData.title}" foi reportada com sucesso.`, 'info', currentUserId);
+ }
+
+ showNotification('Nova ocorrência reportada', 'warning');
+ handleCloseModal();
+ } catch (error) {
+ console.error("Erro ao reportar ocorrência:", error);
+ showNotification("Erro ao reportar ocorrência.", "error");
+ }
+ };
+
+ const handleSaveFatura = async (e) => {
+ e.preventDefault();
+ if (!formData.moradorId || !formData.categoria || !formData.valor || !formData.dataVencimento) {
+ showNotification("Preencha todos os campos obrigatórios.", "error");
+ return;
+ }
+ try {
+ const morador = residents.find(r => r.id === formData.moradorId);
+ if (!morador) return;
+
+ const valor = Number(formData.valor);
+ const newFaturaRef = push(ref(db, 'faturas'));
+ await set(newFaturaRef, {
+ moradorId: morador.id,
+ nomeMorador: morador.name,
+ fracao: morador.unit,
+ categoria: formData.categoria,
+ valor: valor,
+ dataVencimento: formData.dataVencimento,
+ status: 'Pendente',
+ dataEmissao: new Date().toISOString().split('T')[0]
+ });
+
+ const newPending = (Number(morador.pending) || 0) + valor;
+ await set(ref(db, `condominos/${morador.id}/pending`), newPending);
+
+ 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');
+
+ showNotification(`Fatura de ${valor.toFixed(2)}€ emitida para ${morador.name}`);
+ handleCloseModal();
+ } catch (error) {
+ console.error("Erro ao emitir fatura:", error);
+ showNotification("Erro ao emitir fatura.", "error");
+ }
+ };
+
+ 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");
+ } catch (error) {
+ console.error("Erro ao pagar fatura:", error);
+ showNotification("Erro ao processar pagamento.", "error");
+ }
+ };
+
+ const handleApproveFatura = async (fatura) => {
+ try {
+ 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 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');
+ showNotification("Pagamento aprovado com sucesso!", "success");
+ } catch (error) {
+ console.error("Erro ao aprovar fatura:", error);
+ showNotification("Erro ao processar aprovação.", "error");
+ }
+ };
+
+ const handleResolveIssue = async (id) => {
+ try {
+ const issue = issues.find(i => i.id === id);
+ if (issue) {
+ await set(ref(db, `manutencao/${id}`), { ...issue, status: 'Resolvido' });
+ sendSystemNotification(`A manutenção "${issue.title}" foi concluída com sucesso.`, 'success', 'todos');
+ showNotification('Ocorrência resolvida com sucesso');
+ }
+ } catch (error) {
+ console.error("Erro ao resolver ocorrência:", error);
+ showNotification("Erro ao resolver ocorrência.", "error");
+ }
+ };
+
+ const handleSaveBooking = async (e) => {
+ e.preventDefault();
+ if (!formData.resident || !formData.date || !formData.time) {
+ showNotification("Preencha todos os campos obrigatórios.", "error");
+ return;
+ }
+ try {
+ const facilityNames = { 'gym': 'Ginásio', 'hall': 'Salão de Festas', 'park': 'Parque de Jogos' };
+ const bookingData = {
+ ...formData,
+ facilityName: facilityNames[formData.facility],
+ status: 'Confirmado'
+ };
+
+ const newBookingRef = push(ref(db, 'reservas'));
+ await set(newBookingRef, bookingData);
+
+ if (bookingData.cost > 0) {
+ const newIncomeRef = push(ref(db, 'financas'));
+ await set(newIncomeRef, {
+ type: 'income',
+ category: `Reserva: ${bookingData.facilityName}`,
+ date: bookingData.date,
+ amount: bookingData.cost,
+ desc: `Reserva por ${bookingData.resident}`
+ });
+ }
+
+ sendSystemNotification(`Nova reserva: ${bookingData.facilityName} a ${bookingData.date}`, 'info', 'admin');
+ if (userRole !== 'admin') {
+ sendSystemNotification(`A sua reserva para ${bookingData.facilityName} foi confirmada.`, 'success', currentUserId);
+ }
+
+ showNotification(`Reserva confirmada para ${bookingData.facilityName}`);
+ handleCloseModal();
+ } catch (error) {
+ console.error("Erro ao criar reserva:", error);
+ showNotification("Erro ao criar reserva.", "error");
+ }
+ };
+
+ const handleGenerateInvoice = async (resident) => {
+ if (resident.pending <= 0) {
+ showNotification(`Não há dívidas para a fração ${resident.unit}`, 'warning');
+ return;
+ }
+ try {
+ const newInvoiceRef = push(ref(db, 'faturacao'));
+ await set(newInvoiceRef, {
+ residentId: resident.id,
+ unit: resident.unit,
+ name: resident.name,
+ amount: Number(resident.pending),
+ date: new Date().toISOString().split('T')[0],
+ status: 'Emitida'
+ });
+
+ 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');
+
+ showNotification(`Fatura instantânea gerada para a fração ${resident.unit}`, 'success');
+ } catch (error) {
+ console.error("Erro ao faturar:", error);
+ showNotification("Erro ao gerar fatura.", "error");
+ }
+ };
+
+ const DashboardView = () => (
+
+
+ {userRole === 'admin' ? (
+ = 0 ? 'up' : 'down'} trendValue="Atual" color="bg-blue-500" />
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
Próximas Reservas
+ setActiveTab('all_bookings')}>Ver todas as Reservas
+
+
+ {bookings.slice(0, 4).map(booking => (
+
+
+
+ {booking.facility === 'gym' ?
: booking.facility === 'hall' ?
:
}
+
+
+
{booking.facilityName}
+
{booking.date} • {booking.time}
+
+
+
+
+ ))}
+
+
+
+
+
+
Quadro de Avisos
+ setActiveTab('maintenance')}>{userRole === 'admin' ? 'Gerir' : 'Ver Ocorrências'}
+
+
+ {issues.slice(0, 3).map((issue) => (
+
+
+
+ {issue.date}
+
+
{issue.title}
+
{issue.location}
+
+ ))}
+
+
+
+
+ );
+
+ const BookingView = ({ facilityType, title, icon: Icon, description, priceInfo, color }) => {
+ const facilityBookings = bookings.filter(b => b.facility === facilityType);
+
+ return (
+
+
+
+
+
+
+
{title}
+
{description}
+
+ Horário: 08:00 - 22:00
+ {priceInfo}
+
+
+
handleOpenModal('booking', null, facilityType)}
+ className="bg-white text-slate-900 px-6 py-3 rounded-lg font-bold hover:bg-slate-50 transition-colors shadow-lg flex items-center gap-2"
+ >
+ Reservar Agora
+
+
+
+
+
Agenda de Reservas
+ {facilityBookings.length === 0 ? (
+
+
+
Sem reservas agendadas para este espaço.
+
+ ) : (
+
+ {facilityBookings.map(booking => (
+
+
+
+ {booking.date}
+
+
+
{booking.time}
+
+ {booking.resident}
+
+ {booking.cost > 0 && (
+
+ Custo:
+ {booking.cost}€
+
+ )}
+
+ ))}
+
+ )}
+
+
+ );
+ };
+
+ const MapView = () => (
+
+
+
+
Mapa do Condomínio
+
Plantas e Localizações
+
+
+
+
+
+
+
+
+
VIA CENTRAL
+
+
+
+
Bloco A
+
10 andares • 20 Frações
+
+
+
+
+
Bloco B
+
8 andares • 16 Frações
+
+
+
+
+
Parque de Jogos
+
Campo Polidesportivo
+
Clique para reservar
+
+
+
+
+
+
+ Recepção
+
+
+
+
+ );
+
+ const MaintenanceView = () => (
+
+
+
+
Manutenção e Ocorrências
+
Gestão de pedidos e reparações
+
+ {userRole === 'admin' ? (
+
+ Resolver Problemas
+
+ ) : (
+
handleOpenModal('issue')}
+ className="bg-orange-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-orange-700 flex items-center gap-2 shadow-sm transition-colors"
+ >
+ Reportar Problema
+
+ )}
+
+
+
+ {issues.length === 0 ? (
+
+
+
Sem ocorrências registadas.
+
+ ) : (
+
+ {issues.map(issue => (
+
+
+
+
+
+ {issue.date}
+
+
+
{issue.title}
+
+ {issue.location}
+
+
+
+
+ Prioridade {issue.priority}
+
+
+ {userRole === 'admin' && issue.status !== 'Resolvido' && (
+ 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"
+ >
+ Resolver
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+ );
+
+ const ProfileView = ({ theme, setTheme }) => {
+ const [activeSection, setActiveSection] = useState('personal');
+
+ return (
+
+
+ {/* Profile Sidebar */}
+
+
+
+ {userRole === 'admin' ? 'AD' : 'MO'}
+
+
{userRole === 'admin' ? 'Admin Condomínio' : 'Morador'}
+
{userRole === 'admin' ? 'Administrador Geral' : 'Residente'}
+
+
+
setActiveSection('personal')}
+ className={`text-left px-4 py-3 rounded-lg text-sm font-medium transition-colors flex items-center gap-3 ${activeSection === 'personal' ? 'bg-white dark:bg-dark-surface text-blue-600 dark:text-blue-400 shadow-sm' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-dark-card'}`}
+ >
+ Dados Pessoais
+
+
setActiveSection('security')}
+ className={`text-left px-4 py-3 rounded-lg text-sm font-medium transition-colors flex items-center gap-3 ${activeSection === 'security' ? 'bg-white dark:bg-dark-surface text-blue-600 dark:text-blue-400 shadow-sm' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-dark-card'}`}
+ >
+ Segurança
+
+ {userRole === 'admin' && (
+
setActiveSection('permissions')}
+ className={`text-left px-4 py-3 rounded-lg text-sm font-medium transition-colors flex items-center gap-3 ${activeSection === 'permissions' ? 'bg-white dark:bg-dark-surface text-blue-600 dark:text-blue-400 shadow-sm' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-dark-card'}`}
+ >
+ Permissões
+
+ )}
+
setActiveSection('settings')}
+ className={`text-left px-4 py-3 rounded-lg text-sm font-medium transition-colors flex items-center gap-3 ${activeSection === 'settings' ? 'bg-white dark:bg-dark-surface text-blue-600 dark:text-blue-400 shadow-sm' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-dark-card'}`}
+ >
+ Preferências
+
+
+
+ {/* Profile Content */}
+
+ {activeSection === 'personal' && (
+
+
Dados Pessoais
+
+
+
+
+
+
+
+
+
+
+
+ {
+ showNotification('Alterações guardadas com sucesso!', 'success');
+ sendSystemNotification('Um utilizador atualizou os seus dados pessoais.', 'info', 'admin');
+ }} 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
+
+
+
+
+ )}
+
+ {activeSection === 'security' && (
+
+
Segurança
+
+
+
+
Autenticação de Dois Fatores (2FA)
+
Recomendamos ativar o 2FA para maior segurança da sua conta.
+
{
+ showNotification('Autenticação de Dois Fatores ativada com sucesso!', 'success');
+ sendSystemNotification('Um utilizador ativou a autenticação 2FA.', 'success', 'admin');
+ }} className="text-orange-900 dark:text-orange-200 text-xs font-bold underline mt-2">Ativar Agora
+
+
+
+
+
+
+
+
+
+
+ {
+ showNotification('Segurança atualizada com sucesso!', 'success');
+ sendSystemNotification('Um utilizador alterou a palavra-passe.', 'info', 'admin');
+ }} className="bg-slate-800 dark:bg-slate-700 text-white px-6 py-2 rounded-lg font-medium hover:bg-slate-900 dark:hover:bg-slate-600 shadow-sm transition-colors">
+ Atualizar Segurança
+
+
+
+
+ )}
+
+ {activeSection === 'permissions' && (
+
+
Nível de Acesso
+
+
+
+
+
+
+
Acesso Total (Admin)
+
Tem permissões totais para gerir condóminos, finanças e configurações.
+
+
+
+
+
Permissões Específicas:
+ {['Gerir Condóminos (Criar, Editar, Eliminar)', 'Gestão Financeira Completa', 'Moderação de Ocorrências', 'Configuração do Sistema', 'Gestão de Usuários'].map((perm, idx) => (
+
+ ))}
+
+
+ )}
+
+ {activeSection === 'settings' && (
+
+
Preferências da Aplicação
+
+
+
+
+
+
Aparência
+
+
setTheme('light')}
+ className={`border-2 ${theme === 'light' ? 'border-blue-500 bg-blue-50' : 'border-slate-200 bg-slate-50'} p-3 rounded-lg text-center cursor-pointer transition-colors`}
+ >
+
+
Claro
+
+
setTheme('dark')}
+ className={`border-2 ${theme === 'dark' ? 'border-blue-500 bg-blue-50' : 'border-slate-200 bg-slate-50'} p-3 rounded-lg text-center cursor-pointer transition-colors`}
+ >
+
+
Escuro
+
+
setTheme('system')}
+ className={`border-2 ${theme === 'system' ? 'border-blue-500 bg-blue-50' : 'border-slate-200 bg-slate-50'} p-3 rounded-lg text-center cursor-pointer transition-colors`}
+ >
+
+
Sistema
+
+
+
+
+
+ )}
+
+
+
+ );
+ };
+
+ if (!isAuthenticated) {
+ return ;
+ }
+
+ return (
+
+ {/* Mobile Overlay */}
+ {isSidebarOpen && (
+
+ )}
+
+ {/* Sidebar */}
+
+
+ {/* Main Content */}
+
+ {/* Header */}
+
+
+
+
+
+
{
+ activeTab === 'dashboard' ? 'Visão Geral' :
+ activeTab === 'residents' ? 'Condóminos' :
+ activeTab === 'finance' ? 'Gestão Financeira' :
+ activeTab === 'billing' ? 'Faturação e Cobranças' :
+ activeTab === 'maintenance' ? 'Ocorrências e Manutenção' :
+ activeTab === 'messages' ? 'Mensagens e Fórum' :
+ activeTab === 'map' ? 'Mapa do Condomínio' :
+ activeTab === 'all_bookings' ? 'Todas as Reservas' :
+ activeTab === 'gym' ? 'Ginásio' :
+ activeTab === 'hall' ? 'Salão de Festas' :
+ activeTab === 'park' ? 'Parque de Jogos' :
+ activeTab === 'profile' ? 'O Meu Perfil' : activeTab
+ }
+
+
+
+
+
+
+
+
+ {/* Notifications */}
+
+
setNotificationsOpen(!isNotificationsOpen)}
+ >
+
+ {unreadNotifications > 0 && (
+
+ )}
+
+
+ {isNotificationsOpen && (
+
+
+
Notificações
+ Limpar
+
+
+ {notificationsList.length === 0 ? (
+
Sem novas notificações
+ ) : (
+ notificationsList.map(notif => (
+
+
+
+
+
{notif.message}
+
+
{notif.time}
+ {!notif.read && (
+
handleMarkAsRead(notif.id)} className="text-[10px] text-blue-600 dark:text-blue-400 hover:underline">
+ Marcar como lida
+
+ )}
+
+
+
+
+ ))
+ )}
+
+
+ )}
+
+
+
setActiveTab('profile')}
+ title="Meu Perfil"
+ >
+ {userRole === 'admin' ? 'AD' : 'MO'}
+
+
+
+
+ {/* Content Body */}
+
+
+
+ {/* --- DASHBOARD --- */}
+ {activeTab === 'dashboard' &&
}
+
+ {/* --- MAPA --- */}
+ {activeTab === 'map' &&
}
+
+ {/* --- GINÁSIO --- */}
+ {activeTab === 'gym' && (
+
+ )}
+
+ {/* --- SALÃO --- */}
+ {activeTab === 'hall' && (
+
+ )}
+
+ {/* --- PARQUE --- */}
+ {activeTab === 'park' && (
+
+ )}
+
+ {/* --- ALL BOOKINGS --- */}
+ {activeTab === 'all_bookings' && (
+
+
+
+
Histórico de Reservas
+
Lista completa de agendamentos em todos os espaços de lazer
+
+
setActiveTab('map')} className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 flex items-center justify-center gap-2 shadow-sm transition-colors">
+ Nova Reserva
+
+
+
+ {bookings.map(booking => (
+
+
+
+ {booking.facility === 'gym' ?
: booking.facility === 'hall' ?
:
}
+
+
+
{booking.facilityName}
+
{booking.date} • {booking.time}
+
+
+
+
{booking.resident}
+
+
+
+
+
+ ))}
+ {bookings.length === 0 && (
+
+
+
Sem reservas
+
Ainda não existem agendamentos no condomínio.
+
+ )}
+
+
+ )}
+ {/* --- APPROVALS --- */}
+ {activeTab === 'approvals' && userRole === 'admin' && (
+
+
+
Pagamentos Concluídos
+
Consulte o histórico de todos os pagamentos concluídos pelos condóminos.
+
+
+
+
+
+
+ Morador
+ Fatura
+ Estado
+ Valor
+
+
+
+ {faturas.filter(f => f.status === 'Pago').map(fatura => (
+
+
+ {fatura.nomeMorador}
+ Fração: {fatura.fracao}
+
+
+ {fatura.categoria}
+ Venceu a: {fatura.dataVencimento}
+
+
+
+ Pago
+
+
+ {Number(fatura.valor).toFixed(2)}€
+
+ ))}
+ {faturas.filter(f => f.status === 'Pago').length === 0 && (
+ Nenhum pagamento concluído encontrado.
+ )}
+
+
+
+
+
+ )}
+
+ {/* --- RESIDENTS --- */}
+ {activeTab === 'residents' && (
+
+
+
+
Gestão de Condóminos
+
Total: {residents.length} frações registadas
+
+
+
+
+ setSearchQuery(e.target.value)}
+ className="pl-10 pr-4 py-2 border border-slate-200 dark:border-dark-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-full sm:w-64 bg-white dark:bg-dark-card dark:text-white dark:placeholder-slate-500 transition-colors"
+ />
+
+
handleOpenModal('resident')}
+ className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 flex items-center justify-center gap-2 transition-colors shadow-sm"
+ >
+ Adicionar
+
+
+
+
+
+
+
+ Fração
+ Proprietário
+ Contacto
+ Estado Quotas
+ Acesso
+ Em Dívida
+ Ações
+
+
+
+ {filteredResidents.map((resident) => (
+
+ {resident.unit}
+
+
+ {resident.name}
+ {resident.email}
+
+
+ {resident.contact}
+
+
+ handleToggleRole(resident.id)}
+ className={`px-3 py-1 rounded-full text-xs font-bold transition-colors ${resident.role === 'admin' ? 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400 dark:hover:bg-purple-900/50' : 'bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700'}`}
+ title="Mudar Permissões"
+ >
+ {resident.role === 'admin' ? 'Admin' : 'Morador'}
+
+
+ 0 ? 'text-red-600 dark:text-red-400' : 'text-slate-600 dark:text-slate-400'}`}>
+ {Number(resident.pending).toFixed(2)}€
+
+
+
+ handleOpenModal('resident', resident)}
+ className="p-1.5 text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/30 rounded-lg transition-colors" title="Editar"
+ >
+
+
+ handleDeleteResident(resident.id)}
+ className="p-1.5 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors" title="Eliminar"
+ >
+
+
+
+
+
+ ))}
+
+
+
+
+
+ )}
+
+ {/* --- BILLING / COBRANÇAS --- */}
+ {activeTab === 'billing' && userRole === 'admin' && (
+
+
+
+
Avisos de Cobrança
+
Emita faturas ou avise condóminos individualmente
+
+
handleOpenModal('emitir_fatura')} className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 flex items-center justify-center gap-2 shadow-sm">
+ Emitir Fatura
+
+
+
+
+
+
+ Fração
+ Condómino
+ Quotas em Atraso
+ Ações
+
+
+
+ {residents.map((resident) => (
+
+ {resident.unit}
+ {resident.name}
+ 0 ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-500'}`}>
+ {resident.pending > 0 ? `${Number(resident.pending).toFixed(2)}€` : 'Regularizado'}
+
+
+ {
+ showNotification(`Aviso de cobrança enviado para ${resident.email}`, 'success');
+ sendSystemNotification(`Aviso de cobrança enviado a ${resident.name} para a fração ${resident.unit}.`, 'info', resident.id);
+ }}
+ className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-colors shadow-sm cursor-pointer ${resident.pending > 0 ? 'bg-orange-600 text-white hover:bg-orange-700' : 'bg-slate-200 text-slate-400 cursor-not-allowed dark:bg-slate-800 dark:text-slate-600'}`}
+ disabled={resident.pending <= 0}
+ >
+ Notificar
+
+ handleGenerateInvoice(resident)}
+ className="ml-2 px-3 py-1.5 rounded-lg text-xs font-bold transition-colors shadow-sm bg-slate-800 text-white hover:bg-slate-900 dark:bg-slate-700 dark:hover:bg-slate-600"
+ >
+ Faturar na Hora
+
+ {
+ showNotification(`Fatura enviada com sucesso para ${resident.email}`, 'success');
+ sendSystemNotification(`Fatura enviada a ${resident.name} (${resident.unit}) por email.`, 'info', resident.id);
+ }}
+ className="ml-2 px-3 py-1.5 rounded-lg text-xs font-bold transition-colors shadow-sm bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700"
+ >
+ Enviar por Email
+
+
+
+ ))}
+
+
+
+
+ )}
+ {/* --- MINHAS CONTAS (Morador) --- */}
+ {activeTab === 'minhas_contas' && userRole === 'morador' && (
+
+
+
+
+
Total Pendente
+
+ {faturas.filter(f => f.moradorId === currentUserId && f.status === 'Pendente').reduce((acc, f) => acc + Number(f.valor), 0).toFixed(2)}€
+
+
+
+
+
+
+
Total Pago
+
+ {faturas.filter(f => f.moradorId === currentUserId && f.status === 'Pago').reduce((acc, f) => acc + Number(f.valor), 0).toFixed(2)}€
+
+
+
+
+
+
+
+
+
+
Minhas Faturas
+
Consulte as suas despesas e faturas emitidas
+
+
+
+
+
+
+ Data Emissão
+ Categoria
+ Vencimento
+ Valor
+ Estado
+ Ações
+
+
+
+ {faturas.filter(f => f.moradorId === currentUserId).length === 0 ? (
+
+
+ Nenhuma fatura encontrada.
+
+
+ ) : (
+ faturas.filter(f => f.moradorId === currentUserId).map((fatura) => (
+
+ {fatura.dataEmissao}
+ {fatura.categoria}
+ {fatura.dataVencimento}
+ {Number(fatura.valor).toFixed(2)}€
+
+
+ {fatura.status === 'Pendente' ? (
+ 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"
+ >
+ Pagar
+
+ ) : fatura.status === 'Em Validação' ? (
+
+ Em Validação
+
+ ) : (
+
+ Pago
+
+ )}
+
+
+ ))
+ )}
+
+
+
+
+
+ )}
+
+ {/* --- FINANCES --- */}
+ {/* --- FINANCES --- */}
+ {activeTab === 'finance' && (
+
+
+
+
+
Receitas (Global)
+
{totalIncome.toFixed(2)}€
+
+
+
+
+
+
Despesas (Global)
+
{totalExpense.toFixed(2)}€
+
+
+
+
+
+
Balanço Líquido
+
= 0 ? 'text-blue-600 dark:text-blue-400' : 'text-red-600 dark:text-red-400'}`}>
+ {balance > 0 ? '+' : ''}{balance.toFixed(2)}€
+
+
+
+
+
+
+
+
+
+
Diário Financeiro
+ {finances.length} movimentos
+
+
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"
+ >
+ Novo Registo
+
+
+
+
+
+
+ Data
+ Categoria
+ Descrição
+ Tipo
+ Valor
+ Recibo
+
+
+
+ {finances.map((item) => (
+
+ {item.date}
+ {item.category}
+ {item.desc}
+
+
+
+
+ {item.type === 'income' ? '+' : '-'}{Number(item.amount).toFixed(2)}€
+
+
+ {
+ showNotification(`Recibo de ${item.category} descarregado.`, 'success');
+ sendSystemNotification(`Recibo de ${item.category} descarregado.`, 'info', currentUserId);
+ }} className="text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors" title="Descarregar Recibo">
+
+
+
+
+ ))}
+
+
+
+
+
+ )}
+
+ {/* --- MESSAGES --- */}
+ {activeTab === 'messages' && (
+
+ {/* Contact List */}
+
+
+
Conversas
+
{ setIsCreateGroupModalOpen(true); setNewGroupName(''); setNewGroupMembers([]); }} className="p-2 bg-blue-100 text-blue-600 rounded-lg hover:bg-blue-200 dark:bg-blue-900/40 dark:text-blue-400 transition-colors" title="Criar Grupo">
+
+
+
+
+
+
setActiveChat({ type: 'global', id: 'global', name: 'Fórum do Condomínio' })}
+ className={`p-3 border-b-2 cursor-pointer transition-colors ${activeChat.type === 'global' ? 'border-blue-100 dark:border-blue-900/30 bg-blue-50/50 dark:bg-blue-900/10 hover:bg-blue-50/80 dark:hover:bg-blue-900/20' : 'border-transparent hover:bg-slate-50 dark:hover:bg-dark-card'}`}
+ >
+
+
+
+
+
+
+
Fórum do Condomínio
+ Geral
+
+
Grupo partilhado
+
+
+
+ {chatGroups.filter(g => g.members && (Object.values(g.members).map(String).includes(String(currentUserId)) || userRole === 'admin')).map(group => (
+
setActiveChat({ type: 'group', id: group.id, name: group.name })}
+ className={`p-3 border-b border-slate-50 dark:border-dark-border/50 cursor-pointer transition-colors ${activeChat.id === group.id ? 'bg-blue-50/50 dark:bg-blue-900/10' : 'hover:bg-slate-50 dark:hover:bg-dark-card'}`}
+ >
+
+
+
+
+
+
+
{group.name}
+ {activeChat.id === group.id && }
+
+
Grupo
+
+
+
+ ))}
+ {residents.filter(r => r.id !== currentUserId).map(res => (
+
setActiveChat({ type: 'private', id: res.id, name: res.name })}
+ className={`p-3 border-b border-slate-50 dark:border-dark-border/50 cursor-pointer transition-colors ${activeChat.id === res.id ? 'bg-blue-50/50 dark:bg-blue-900/10' : 'hover:bg-slate-50 dark:hover:bg-dark-card'}`}
+ >
+
+
+ {res.name.substring(0, 2).toUpperCase()}
+
+
+
+
{res.name} {res.unit && `(${res.unit})`}
+ {activeChat.id === res.id && }
+
+
Morador
+
+
+
+ ))}
+
+
+
+ {/* Chat Area */}
+
+
+
+
+ {activeChat.type === 'global' || activeChat.type === 'group' ? : activeChat.name.substring(0, 2).toUpperCase()}
+
+
+
{activeChat.name}
+
{activeChat.type === 'global' ? 'Todos os moradores' : activeChat.type === 'group' ? 'Grupo Privado' : 'Privado'}
+
+
+
+
+
+
+
Mensagens
+
+ {messages.map((msg) => {
+ const isMe = msg.senderId === currentUserId;
+ const timeString = new Date(msg.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
+
+ return (
+
+
+ {!isMe && (
+
+ {msg.senderName}
+
+ )}
+
{msg.text}
+
{timeString}
+
+
+ );
+ })}
+
+
+
+
+
+ )}
+
+ {/* --- MAINTENANCE --- */}
+ {activeTab === 'maintenance' &&
}
+
+ {/* --- PROFILE --- */}
+ {activeTab === 'profile' &&
}
+
+
+
+
+ {/* --- Toast Notification --- */}
+ {notification && (
+
+ {notification.type === 'success' ?
:
}
+
{notification.message}
+
+ )}
+
+ {/* --- Modals --- */}
+
+ {/* Resident Modal */}
+
+
+
+
+ {/* Emitir Fatura Modal */}
+
+
+
+
+ {/* Finance Modal */}
+
+
+
+
+ {/* Issue Modal */}
+
+
+
+
+ {/* Booking Modal */}
+
+
+
+
+ setIsCreateGroupModalOpen(false)} title="Criar Novo Grupo">
+
+
+
+
+
+ );
+ }
+
+ const root = createRoot(document.getElementById('root'));
+ root.render( );
+
+
diff --git a/sw.js b/sw.js
index e06cdd4..0411acc 100644
--- a/sw.js
+++ b/sw.js
@@ -1,4 +1,4 @@
-const CACHE_NAME = 'condopro-v1';
+const CACHE_NAME = 'mycondominium-v3';
const ASSETS_TO_CACHE = [
'./',
'./index.html',
@@ -14,15 +14,43 @@ const ASSETS_TO_CACHE = [
];
self.addEventListener('install', (event) => {
+ self.skipWaiting(); // Force the waiting service worker to become the active service worker.
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(ASSETS_TO_CACHE))
);
});
-self.addEventListener('fetch', (event) => {
- event.respondWith(
- caches.match(event.request)
- .then((response) => response || fetch(event.request))
+self.addEventListener('activate', (event) => {
+ event.waitUntil(clients.claim()); // Claim clients immediately so updates are visible without reloading all tabs
+ event.waitUntil(
+ caches.keys().then((cacheNames) => {
+ return Promise.all(
+ cacheNames.map((cacheName) => {
+ if (cacheName !== CACHE_NAME) {
+ return caches.delete(cacheName);
+ }
+ })
+ );
+ })
+ );
+});
+
+self.addEventListener('fetch', (event) => {
+ // Network First strategy: try network, if it fails, fallback to cache
+ event.respondWith(
+ fetch(event.request).then((networkResponse) => {
+ // If request is successful, update the cache
+ if (networkResponse && networkResponse.status === 200 && event.request.method === 'GET') {
+ const responseToCache = networkResponse.clone();
+ caches.open(CACHE_NAME).then((cache) => {
+ cache.put(event.request, responseToCache);
+ });
+ }
+ return networkResponse;
+ }).catch(() => {
+ // If network fails (offline), return cached version
+ return caches.match(event.request);
+ })
);
});
diff --git a/temp_script.jsx b/temp_script.jsx
new file mode 100644
index 0000000..4383d1d
--- /dev/null
+++ b/temp_script.jsx
@@ -0,0 +1,2335 @@
+
+
+
+