From f6b7a98471bb9c718c8091a62fa65243aceb92b7 Mon Sep 17 00:00:00 2001 From: Ricardo <230414@epvc.pt> Date: Wed, 6 May 2026 12:43:37 +0100 Subject: [PATCH] ajustes --- .firebase/hosting..cache | 35 +- .firebaserc | 2 +- .vscode/settings.json | 2 +- README.md | 4 +- RELATORIO_TECNICO.md | 6 +- firebase.json | 9 + index.html | 151 ++- sw.js | 38 +- temp_script.jsx | 2335 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 2509 insertions(+), 73 deletions(-) create mode 100644 temp_script.jsx 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} +
+ +
+ ); + } + 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)}€ - -
- - + +
+ 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
- +
+ {finances.length === 0 && ( + + )} + +
@@ -2350,7 +2401,11 @@ } const root = createRoot(document.getElementById('root')); - root.render(); + 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 @@ + + + + + + + + MyCondominium + + + tailwind.config = { + darkMode: 'class', + theme: { + extend: { + colors: { + dark: { + bg: '#0f172a', + surface: '#1e293b', + card: '#334155', + border: '#475569', + text: '#f1f5f9', + mute: '#94a3b8' + } + } + } + } + } + + { + "imports": { + "react": "https://esm.sh/react@18.2.0", + "react-dom/client": "https://esm.sh/react-dom@18.2.0/client", + "lucide-react": "https://esm.sh/lucide-react@0.292.0" + } + } + + + + + + +
+ + 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 }) => ( +
+ + {options ? ( + + ) : ( + + )} +
+ ); + + const SidebarItem = ({ icon: Icon, label, active, onClick }) => ( + + ); + + const Card = ({ title, value, icon: Icon, trend, trendValue, color, subtitle }) => ( +
+
+
+

{title}

+

{value}

+
+
+ +
+
+
+
+ {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

+
+ +
+
+ + setEmail(e.target.value)} + className="w-full px-4 py-3 rounded-lg border border-slate-300 dark:border-dark-border focus:ring-2 focus:ring-blue-500 bg-white dark:bg-dark-card text-slate-900 dark:text-white transition-colors" + placeholder="Endereço de email" + autoFocus + required + /> +
+
+ + setPassword(e.target.value)} + className="w-full px-4 py-3 rounded-lg border border-slate-300 dark:border-dark-border focus:ring-2 focus:ring-blue-500 bg-white dark:bg-dark-card text-slate-900 dark:text-white transition-colors" + placeholder="Senha de acesso" + required + /> +
+ {error && ( +
+ + {error} +
+ )} +
+ +
+ +
+
+ ); + }; + + 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

+ +
+
+ {bookings.slice(0, 4).map(booking => ( +
+
+
+ {booking.facility === 'gym' ? : booking.facility === 'hall' ? : } +
+
+

{booking.facilityName}

+

{booking.date} • {booking.time}

+
+
+
+

{booking.resident}

+ +
+
+ ))} +
+
+ +
+
+

Quadro de Avisos

+ +
+
+ {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} +
+
+ +
+ +
+

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

+
+
+
Comum
+
Blocos
+
+
+ +
+
+
+ +
VIA CENTRAL
+ +
+ + Bloco A +
10 andares • 20 Frações
+
+ +
+ + Bloco B +
8 andares • 16 Frações
+
+ +
+ + Parque de Jogos + Campo Polidesportivo +
Clique para reservar
+
+ +
+
+ + Salão Festas +
+
+
+ + Ginásio +
+
+ +
+ + Recepção +
+
+
+
+ ); + + const MaintenanceView = () => ( +
+
+
+

Manutenção e Ocorrências

+

Gestão de pedidos e reparações

+
+ {userRole === 'admin' ? ( + + ) : ( + + )} +
+ +
+ {issues.length === 0 ? ( +
+ +

Sem ocorrências registadas.

+
+ ) : ( +
+ {issues.map(issue => ( +
+
+ +
+ + {issue.date} +
+ +

{issue.title}

+

+ {issue.location} +

+ +
+ + Prioridade {issue.priority} + + + {userRole === 'admin' && issue.status !== 'Resolvido' && ( + + )} +
+
+ ))} +
+ )} +
+
+ ); + + 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'}

+
+ + + + {userRole === 'admin' && ( + + )} + +
+ + {/* Profile Content */} +
+ {activeSection === 'personal' && ( +
+

Dados Pessoais

+
+
+ + +
+
+ + +
+ +
+ +
+
+
+ )} + + {activeSection === 'security' && ( +
+

Segurança

+
+ +
+

Autenticação de Dois Fatores (2FA)

+

Recomendamos ativar o 2FA para maior segurança da sua conta.

+ +
+
+ +
+ +
+ + +
+
+ +
+
+
+ )} + + {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) => ( +
+
+ {perm} +
+ ))} +
+
+ )} + + {activeSection === 'settings' && ( +
+

Preferências da Aplicação

+ +
+
+

Notificações

+
+ + + +
+
+ +
+

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 */} +
+ + + {isNotificationsOpen && ( +
+
+

Notificações

+ +
+
+ {notificationsList.length === 0 ? ( +
Sem novas notificações
+ ) : ( + notificationsList.map(notif => ( +
+
+
+
+

{notif.message}

+
+

{notif.time}

+ {!notif.read && ( + + )} +
+
+
+
+ )) + )} +
+
+ )} +
+ +
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

+
+ +
+
+ {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.

+
+
+
+
+ + + + + + + + + + {faturas.filter(f => f.status === 'Pago').map(fatura => ( + + + + + + + ))} + {faturas.filter(f => f.status === 'Pago').length === 0 && ( + + )} + +
MoradorFaturaEstadoValor
+

{fatura.nomeMorador}

+

Fração: {fatura.fracao}

+
+

{fatura.categoria}

+

Venceu a: {fatura.dataVencimento}

+
+
+ Pago +
+
{Number(fatura.valor).toFixed(2)}€
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" + /> +
+ +
+
+
+ + + + + + + + + + + + + + {filteredResidents.map((resident) => ( + + + + + + + + + + ))} + +
FraçãoProprietárioContactoEstado QuotasAcessoEm DívidaAções
{resident.unit} +
+ {resident.name} + {resident.email} +
+
{resident.contact} + + 0 ? 'text-red-600 dark:text-red-400' : 'text-slate-600 dark:text-slate-400'}`}> + {Number(resident.pending).toFixed(2)}€ + +
+ + +
+
+ +
+
+ )} + + {/* --- BILLING / COBRANÇAS --- */} + {activeTab === 'billing' && userRole === 'admin' && ( +
+
+
+

Avisos de Cobrança

+

Emita faturas ou avise condóminos individualmente

+
+ +
+
+ + + + + + + + + + + {residents.map((resident) => ( + + + + + + + ))} + +
FraçãoCondóminoQuotas em AtrasoAções
{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'} + + + + +
+
+
+ )} + {/* --- 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

+
+
+
+ + + + + + + + + + + + + {faturas.filter(f => f.moradorId === currentUserId).length === 0 ? ( + + + + ) : ( + faturas.filter(f => f.moradorId === currentUserId).map((fatura) => ( + + + + + + + + + )) + )} + +
Data EmissãoCategoriaVencimentoValorEstadoAções
+ Nenhuma fatura encontrada. +
{fatura.dataEmissao}{fatura.categoria}{fatura.dataVencimento}{Number(fatura.valor).toFixed(2)}€ + {fatura.status === 'Pendente' ? ( + + ) : 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 +
+ +
+
+ + + + + + + + + + + + + {finances.map((item) => ( + + + + + + + + + ))} + +
DataCategoriaDescriçãoTipoValorRecibo
{item.date}{item.category}{item.desc} + + + {item.type === 'income' ? '+' : '-'}{Number(item.amount).toFixed(2)}€ + + +
+
+
+
+ )} + + {/* --- MESSAGES --- */} + {activeTab === 'messages' && ( +
+ {/* Contact List */} +
+
+

Conversas

+ +
+
+
+ + +
+
+
+
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} +
+
+ ); + })} +
+ +
+
{ + e.preventDefault(); + if (!newMessageText.trim()) return; + + try { + const path = activeChat.type === 'global' + ? 'mural_mensagens' + : activeChat.type === 'group' + ? `mensagens_grupo/${activeChat.id}` + : `mensagens_privadas/${[currentUserId, activeChat.id].sort().join('_')}`; + + const newMsgRef = push(ref(db, path)); + await set(newMsgRef, { + text: newMessageText, + senderId: currentUserId, + senderName: currentUserName, + role: userRole, + timestamp: Date.now() + }); + setNewMessageText(''); + } catch (error) { + console.error("Erro ao enviar mensagem:", error); + showNotification("Erro ao enviar mensagem.", "error"); + } + }} className="flex gap-2"> + + setNewMessageText(e.target.value)} + placeholder="Escreva a sua mensagem..." + className="flex-1 bg-slate-50 dark:bg-dark-bg border border-slate-200 dark:border-dark-border rounded-full px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:text-white" + /> + +
+
+
+
+ )} + + {/* --- 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 */} + +
+ +
+ + +
+ + +
+ Custo Estimado: + {formData.cost || 0}€ +
+ + + +
+ + setIsCreateGroupModalOpen(false)} title="Criar Novo Grupo"> +
{ + e.preventDefault(); + if (!newGroupName.trim() || newGroupMembers.length === 0) { + showNotification('Selecione um nome e pelo menos um membro.', 'warning'); + return; + } + try { + const groupId = 'grupo_' + Date.now(); + const allMembers = [...newGroupMembers, currentUserId]; + await set(ref(db, `grupos_chat/${groupId}`), { + id: groupId, + name: newGroupName, + members: allMembers, + createdBy: currentUserId, + timestamp: Date.now() + }); + setIsCreateGroupModalOpen(false); + setActiveChat({ type: 'group', id: groupId, name: newGroupName }); + showNotification('Grupo criado com sucesso.', 'success'); + } catch (err) { + showNotification('Erro ao criar grupo.', 'error'); + } + }}> + setNewGroupName(e.target.value)} required /> + + +
+ {residents.filter(r => r.id !== currentUserId).map(r => ( +
+ { + if (e.target.checked) setNewGroupMembers([...newGroupMembers, r.id]); + else setNewGroupMembers(newGroupMembers.filter(id => id !== r.id)); + }} + className="w-4 h-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500" + /> + +
+ ))} +
+ + +
+ + + + ); + } + + const root = createRoot(document.getElementById('root')); + root.render(); + + +