correção.geral
This commit is contained in:
96
.firebase/hosting..cache
Normal file
96
.firebase/hosting..cache
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
test_nif.js,1777390754764,b50e0b4fe7732186ea056d183a0f2bbe7026cf79d384ce66391f746fe6969af6
|
||||||
|
sw.js,1770114976862,b003899c6761f1320628a7e6048429714950195393f134df16159399586d3ca4
|
||||||
|
style.css,1770114976862,4c2e2686b637f6f2f060298dfbabf690219284ff4c5c027711c5b443dde07332
|
||||||
|
script.js,1776937115126,4b08e5f41663ef287d352039798448e36a3c52c60014d58f7ed31471dab4066d
|
||||||
|
manifest.json,1770114976862,eb6e5b596d2a562026e361e5ee5bd1f4c3fc94a5e3b8cfc9d8761c6d21b2b991
|
||||||
|
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
|
||||||
|
.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/refs/remotes/origin/HEAD,1773830493701,0f5d56efe56c5dcabb387d965aad58d0f60a3b7485cb9b04bef04b93bebf911e
|
||||||
|
.git/refs/heads/main,1777903218136,30437e7b9aa4378a80a4ab39abb8a4925238b5ab69719fac495bdb8f4ee17616
|
||||||
|
.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/ea/2dee2f517449595d633889a410cc2aefa2b513,1776937200147,9392a6f5dd0687166fd9b60a4f28176604046d293196fe53038e9d2e456d5336
|
||||||
|
.git/objects/e8/5b891f05021af7f78059851bac361ba110e459,1777903218123,c0684017afd5d4cb4234a4191773d088b5ae747e88b477cabcd08fff53e24450
|
||||||
|
.git/objects/e0/6cdd4ddfacadcef9577916c84406559b985623,1773161195362,d89c95c83f040e788bd182a67bc3cbb9afdf7d3740a388cc4d43d2514b80e556
|
||||||
|
.git/objects/da/444cce2c8426f321e774e9bc8134642bae50ae,1773830457778,293cb0fb7b39d4ffbece90d8f57bcd730c29d754a284c7d83436a489661e507e
|
||||||
|
.git/objects/d3/f58873ab4f70d24d92349c7e85c7df5c7bb7c2,1776937200165,ca8deaee745fd2e87b4804183acc8c484a36b88db729b2c4cb52f17f99ffb747
|
||||||
|
.git/objects/d0/ca0c1d5c7b38b5a5ec040918ce8ece33dfe52c,1776937200149,9d98989471faf7b255a3da69495d5ff5cfe01302e1626558a570e33a74b8c716
|
||||||
|
.git/objects/c2/51549f632caf9449f632743316c3cf728fffc8,1776937257099,1f81e188648ae90999de0d723593b312ae205c183aa0bfd8562ffc83742a85ab
|
||||||
|
.git/objects/b1/23a037ff8edd8fe29dc828726a53f9b55dcc31,1773161195361,c118b3ce9a20c3b4aa540144f17e7dcc1bc26ff0b576c6e00451941e200feee9
|
||||||
|
.git/objects/aa/64354c06769190bc3e113bd7ee4dfb0bfcdad1,1773830457796,31833bc03904dec3c2a34a49028c56042ee348df266b859968414bc009c83f7f
|
||||||
|
.git/objects/a5/c46d963a0c127d421044eb6e50f1b3c8271d95,1777903218134,32190169472d3bc7ae8b307521f604f6fcdeed486694f0deffa2e9e55dc72e9a
|
||||||
|
.git/objects/a0/de46ff5465329041fcd659cf66cadca4415824,1776937200146,43fc08e82b2fabcf91b8670535d21d353a189c5cbd6fe234369b05fead5c8e5e
|
||||||
|
.git/objects/9e/71203cb36f10248f9fa89f1d11f8335ed55be2,1773830457795,c6ee24879edd639035190699f85cf3cdfeb1a8926218f94b9152470d9e75e25d
|
||||||
|
.git/objects/99/8d174cd16af43e7684399aec34ac75fc6eb7f4,1776326595547,a9a6302756f4d83011553a94d7518d7162818c535e13f15f94868127fd516691
|
||||||
|
.git/objects/94/5d8710c7134fdf4feaa701af8d88bda61db300,1776957069564,b68f1087c81f6a22ae676c795169f34674865c85b4e9631b82db50a73cb190d5
|
||||||
|
.git/objects/92/d9d123244957aa1b1dd1a6f54b9899ec28def8,1773161195399,3f3e3c1f4e739595d33d89aef665c3933ec1af7914020cf0ff6de3e3d41d0109
|
||||||
|
.git/objects/8e/7980160dd6e37a0edf181cb022b86334e205fd,1777393032077,921856f2b98e34016c387ef1f73025bba7ed846c64d8017ec6d2e6e0e9f7c586
|
||||||
|
.git/objects/8c/b6ffc314647094561a18f0af805412260abc3b,1776326595546,a256a28d0e28ac76431ae5406923461a7b4fd7aaac40f5dae947377abc2ca830
|
||||||
|
.git/objects/84/2dd08f73738644fe58eecb5409d5ebd1544efb,1776326595582,b5b51d6c09e2f4e9767489040755ab4e1c4cdce30f46567e4391e0e228f04395
|
||||||
|
.git/objects/7f/0d5ab4d6ff0318cbd6a9c3f8eb57aaac4634a4,1777542176251,16d9b4319d629d55eb92c57e1070cca1671b642d2c780895fc600570fcb5b895
|
||||||
|
.git/objects/7d/dec83df895fd4138fb260363a31d42aca01be7,1777903215823,7e84fefc9f50b243bf6456e585c91637cfc0deab22dd40762c75e3f65bedf077
|
||||||
|
.git/objects/7b/4504f49c73b317f7e178597d5ae793880794d2,1777903218135,58046a9cddd44c7ff9e9e9e6db6d2c9ba98cb929d6e76479db066ffd62126fc8
|
||||||
|
.git/objects/6f/3a2913e199cebe9ace75cf7e5a2818da27fbd3,1773161195356,54d84b384f82f8f1b37c547722e9d71bca19420481ee95702893b002a15d564d
|
||||||
|
.git/objects/64/252d08a5b2f805100200ded406373568933c3a,1773161195359,0ab263a5e07f60a15d277e7928fd89d02c260d1ce0004c58ba591ceaa52d1b8b
|
||||||
|
.git/objects/5d/b40ffe617959a1903859dfe399e1c1db842b80,1777393032076,8c9ba49d0b18a69b91a0b3351dd4bc1cdf21ed71ab83513e02789e12aa94f703
|
||||||
|
.git/objects/59/1baf65bf75cf4d04647e7296aff7725b1531b8,1773161014723,c318202212d24ac44a60f4257c38fe4647e4128015caa8210a9487e2f96d9033
|
||||||
|
.git/objects/58/a564985148007b5e4e7f13e3afa9f79debb771,1776183469621,897ee3588fb4b1fb29c58e5a2cea3d5a31e062f0ed873dca7f9539f83b4fd03f
|
||||||
|
.git/objects/55/2e22a7e54217ad8c7ea90b10195baf9d42a36c,1777542176236,09b86220fbc32f2d59ec5b886201d95ecc1905b5027caf261a17d56e529805aa
|
||||||
|
.git/objects/53/2b75117ba5c1870030b7234464a4d7035b1280,1777387356906,6180ed17b6f6018e7f132ae0d43d32a295fd23bfc5f165ad9b3941b60532077a
|
||||||
|
.git/objects/52/11140c00a533a0d88e4d45fbd05a6cd1b16319,1776183305296,8e592b674b8f411d7b0569ffa976e4ee58f4a375e7d4732e7917ddfecfe8e824
|
||||||
|
.git/objects/4c/cad493cf92a2abffe8e42c69b1a0fb11eb771a,1776183305312,85b3b6d5e020134e9b1b581382ab99cdf7ae726a512fccb77b13041a60e7209e
|
||||||
|
.git/objects/46/c92ead4e560bfac09665e747f104c4e6639e1f,1777387356903,6a32730d91517dc002a328382d21729cc2e09e750ec0fe615b5f9256c10b32a8
|
||||||
|
.git/objects/44/85476e52d470cdaba6e18db9df7591fceb0b13,1776183305294,2e7939525d8c7c114190b2aaeaa1cd712ac0d4153c4222c0c6f2c49a4aeb3e1a
|
||||||
|
.git/objects/40/4bcf86370b3aa54e5c09e50a7be47241fe63ee,1773830230299,f9bae290124afebf31f114d71dc73d407a04a248fff87a729abfa68e807e6cf9
|
||||||
|
.git/objects/3b/cafc02b8f3692fbb5b6d93debe13bb50563c2a,1776937200150,70a72e7716314bf0c914dd3d802a5a0ba4c93a23c179aaa86fb8c660f039e344
|
||||||
|
.git/objects/38/465e5ae301b3ddefabfe22b188c4fee52182c0,1773161195398,dbcf76217184aff41336a8f6530dc63b073bb4235b0fec701dfa33df27a0b402
|
||||||
|
.git/objects/32/1c6b6ef5843bf86598659bb85cf0ef3d63ccc8,1773161000773,7781172d118bad5f4282ecc246a2e432d07e682ebb5652c325a8d26d272f804c
|
||||||
|
.git/objects/2d/a32767f43e7735bca583bf1aa5c7436ae485fe,1773830457795,f1cc7925dc66329b987ae56725551624b57040a581eedabe08e1eb66dbe6e6c2
|
||||||
|
.git/objects/2d/085bcbe50700bbcff79f9186c9c317d9bb1ab4,1773830457780,b8a511b75f969777c2fb6d5e3e0538da8888a4817ae34d0e3192c7fe2695538a
|
||||||
|
.git/objects/2c/fea43d887fc1e25adba99d01fc094e26bb25ae,1777903218123,3ff6da2a0ec8c954f94eeb6423bc392ec995d9b56198c375ad2ac417943dc43c
|
||||||
|
.git/objects/2c/373297bf34839459d13ac1a58a395e4cfff34f,1773161195358,5db1efbe8f4e7cf04bbf485b5093328a766bc5dc442922abf664a62e69d1fc6e
|
||||||
|
.git/objects/2b/d5891243d72a793a41ea51994856446a350f55,1777387356904,0547ff6336463e58b4d266d7a08388b2ee53c501b37d95bf19846f896a3bd1a2
|
||||||
|
.git/objects/2a/f13184106a656f511871672a98dca04e58f04b,1776937200165,207a73dfdc380c9ff77147f77f094d28c1e6471f74fc37175c9cc6dde8ff2b3b
|
||||||
|
.git/objects/2a/a7aa1819e7c69a73c81dadbf3643a3aec6fed5,1776183305313,9e31ebefd7d25152cd322bd673d587b1e1367ae75e01aed02e86d6b58f5a430a
|
||||||
|
.git/objects/25/b0e88e2c1a6db6b52948a4d2fc649e06de8cb2,1776957069549,67160c871e47bc652ab9b8a11b65efe4a559f4dc7a84c16e9aeed10cd44d41b0
|
||||||
|
.git/objects/25/5c1a39f46ad00812b25cf59858aabb19ebfa18,1776957069566,c328ecb00d90ba099ec4534307c0f3177478656f0fc2d21b4e02f6e743cf1773
|
||||||
|
.git/objects/23/cec1dc9936a6d9be11078377c6a7685b26373f,1776183305295,d709b165a1199d66da18ba51afe1471aa7b068d4066284bf28d333f09e0c9670
|
||||||
|
.git/objects/1f/96a9bfae61d4e97fc9f0375975e89f031ce971,1777542176253,10e365b22153e0f123b7086751a36b6779ce2368f721a3edffed8be8b92c4dba
|
||||||
|
.git/objects/1a/6f0a583386ccf1152060c78f3237e4b928ebab,1777903218122,3b445ff37e72f864c5ce690f3b9e417ddfaee3e33dca1ad27390fcdc5e8c8e8d
|
||||||
|
.git/objects/15/5422b0a09c128c529df656111fa3bf4a810999,1776183305292,f3973efacb5d450d6636f40464edc7361a5367cf15fa47ec6f6fdc0958b1548b
|
||||||
|
.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/logs/refs/remotes/origin/HEAD,1773830493702,1eba2cff5035849e216a15d3b6013593fa5ef345a8d76bb2881d83b3cb247576
|
||||||
|
.git/logs/refs/heads/main,1777903218136,0e77927c7f96859d395e49b6ea9f8d7f54c6e6c60d00fef26fbd90459c5fe112
|
||||||
|
.git/info/exclude,1773160274653,a362e375cc3330f10d115cfeb0f90a325219d80a764d57e2c4873f78d1d0b4f5
|
||||||
|
.git/hooks/update.sample,1773160274656,2b0a4f42fa30a128b46ad80e89c1f73b89d58b8abb9e92aee1c35625baccb584
|
||||||
|
.git/hooks/sendemail-validate.sample,1773160274654,4d0768bc11017be6b99d4bb4d34b4c8b2fd7ae8a93d42727591afb6737577db2
|
||||||
|
.git/hooks/push-to-checkout.sample,1773160274656,ede5681c63b8ad6d42fcae835820651b7dfa044bc14fa0da0b7ce9bc54bd7f18
|
||||||
|
.git/hooks/prepare-commit-msg.sample,1773160274655,9b3c070fc0608b04e690a7dd7c17419759fbed06932985aed32e306418e86ed2
|
||||||
|
.git/hooks/pre-receive.sample,1773160274655,b703160503002ea73605a1b54a8183989f66e6530c125fbd1595339048ebd856
|
||||||
|
.git/hooks/pre-rebase.sample,1773160274654,b3f2e1f9de52a3895458d3a6f5e5a1ad139f688bcbc358d6e9d31888f7e84829
|
||||||
|
.git/hooks/pre-push.sample,1773160274656,8109b3dc5744bb0e115ee8e53ff7a7617ce4396aaa646538e068a6f09338447d
|
||||||
|
.git/hooks/pre-merge-commit.sample,1773160274655,0479d217dc6329cbba403c8254db7b9ee6c028f62f55db3641b0690373679b25
|
||||||
|
.git/hooks/pre-commit.sample,1773160274654,c420e0798fd9e788afb2604a87c593e37fb82fa9b417ce8e25b72078bbde3cda
|
||||||
|
.git/hooks/pre-applypatch.sample,1773160274656,defbdbcdbf096439878474d3b12ed4b5a63dec7400ecde5f4b60a496391d64d0
|
||||||
|
.git/hooks/post-update.sample,1773160274655,3d5b0c2026371e835c791d4b25099e98b1d507971ed6bfb639ae56a321309aeb
|
||||||
|
.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
|
||||||
10
.firebaserc
10
.firebaserc
@@ -1,5 +1,15 @@
|
|||||||
{
|
{
|
||||||
"projects": {
|
"projects": {
|
||||||
"default": "condomaster-pro-ed9af"
|
"default": "condomaster-pro-ed9af"
|
||||||
|
},
|
||||||
|
"targets": {
|
||||||
|
"condomaster-pro-ed9af": {
|
||||||
|
"hosting": {
|
||||||
|
"condomaster": [
|
||||||
|
"condomaster-pro-web"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"etags": {}
|
||||||
}
|
}
|
||||||
72
RELATORIO_TECNICO.md
Normal file
72
RELATORIO_TECNICO.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# Relatório Técnico - CondoMaster Pro
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Arquitetura de Código e Tecnologias
|
||||||
|
|
||||||
|
A aplicação foi desenvolvida focada na simplicidade de implementação e execução, dispensando a necessidade de servidores complexos (arquitetura Serverless) e processos de *build* pesados.
|
||||||
|
|
||||||
|
### Tecnologias Utilizadas:
|
||||||
|
* **HTML5 & CSS3:** Estrutura base e definições globais.
|
||||||
|
* **JavaScript (React standalone):** O coração da aplicação (localizado no ficheiro `index.html`). O uso de React importado diretamente pelo browser (`esm.sh/react`) juntamente com o compilador Babel (`@babel/standalone`) permite criar componentes interativos, gerir estados (`useState`, `useEffect`) de forma reativa sem necessitar de instalar Node.js ou usar ferramentas como Vite/Webpack no ambiente de produção final.
|
||||||
|
* **Tailwind CSS (via CDN):** Utilizado para o design visual, garantindo uma interface bonita, responsiva (adaptada a telemóveis) e com suporte automático para Dark Mode (Modo Escuro).
|
||||||
|
* **Lucide-React:** Biblioteca de ícones moderna utilizada ao longo de toda a interface.
|
||||||
|
* **PWA (Progressive Web App):** A aplicação contém um ficheiro `manifest.json` e configurações de `theme-color`, permitindo que os utilizadores a "instalem" nos seus telemóveis como se fosse uma aplicação nativa.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Base de Dados (Backend)
|
||||||
|
|
||||||
|
O projeto não tem um servidor backend tradicional; em vez disso, utiliza a infraestrutura segura e em tempo real da **Google (Firebase)**.
|
||||||
|
|
||||||
|
### 3.1. Autenticação (Firebase Auth)
|
||||||
|
Gere os logins e registos. Garante que apenas utilizadores validados acedem à plataforma.
|
||||||
|
|
||||||
|
### 3.2. Firebase Realtime Database
|
||||||
|
É uma base de dados NoSQL (baseada em documentos JSON) que envia atualizações para todos os utilizadores no exato momento em que algo muda, sem precisarem de fazer refresh à página.
|
||||||
|
|
||||||
|
**Estrutura da Base de Dados (Nós Principais):**
|
||||||
|
1. **`condominos/`**: Guarda a lista de moradores, informações de contacto (NIF, Telemóvel), número da fração e o estatuto de aprovação (pendente vs aprovado).
|
||||||
|
2. **`financas/`**: Registo contabilístico do condomínio (despesas como "Limpeza" e receitas como "Quotas").
|
||||||
|
3. **`manutencao/`**: Pedidos de reparação reportados pelos moradores, com prioridade, localização e estado ("Resolvido", "Em Progresso").
|
||||||
|
4. **`reservas/`**: Calendário de ocupação dos espaços comuns (Salão de Festas, Ginásio, etc.).
|
||||||
|
5. **`faturacao/` e `faturas/`**: Registo e emissão de recibos e avisos de cobrança gerados pelo sistema.
|
||||||
|
6. **`notificacoes/`**: Sistema de alertas (dividido entre pastas `admin` e `ID_do_morador`) para avisar o utilizador de ações pendentes ou sucessos.
|
||||||
|
7. **`mural_mensagens/`**: Histórico do chat "Fórum Geral" onde todos os moradores interagem.
|
||||||
|
8. **`mensagens_privadas/`**: Canais de comunicação 1-para-1. O ID do caminho é gerado combinando os IDs dos dois utilizadores.
|
||||||
|
9. **`grupos_chat/`**: Contém as definições dos grupos criados (Nome do grupo, lista de IDs de membros autorizados e quem o criou).
|
||||||
|
10. **`mensagens_grupo/`**: Mensagens restritas aos grupos de chat criados.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Publicação e Alojamento (Hosting)
|
||||||
|
|
||||||
|
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)
|
||||||
|
* **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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Como Fazer Atualizações (Guia Passo-a-Passo)
|
||||||
|
|
||||||
|
Se quiser editar o código e publicar uma nova versão, o processo é o seguinte:
|
||||||
|
|
||||||
|
1. **Editar Localmente:** Abra os seus ficheiros (`index.html`, `style.css`, etc.) no seu editor de código (como o VS Code) e faça as modificações desejadas. Pode testar abrindo o `index.html` no seu browser localmente.
|
||||||
|
2. **Abrir o Terminal:** No seu Mac, abra o terminal e navegue para a pasta do projeto (`cd /Users/230414/GestorCondominio`).
|
||||||
|
3. **Fazer o Deploy:** Execute o comando que envia os ficheiros novos para o servidor:
|
||||||
|
```bash
|
||||||
|
./firebase deploy --only hosting
|
||||||
|
```
|
||||||
|
4. **Verificar:** Aguarde que o terminal diga `Deploy complete!`. A partir desse segundo, a alteração está disponível mundialmente no link do projeto.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Conclusão
|
||||||
|
|
||||||
|
A plataforma está otimizada para ser expansível, rápida e orientada ao "Real-time". A eliminação da barreira de reloads de página cria a sensação de uma aplicação fluida. Toda a lógica condicional está bem contida dentro da função componente `App()` em React, com validações sólidas como as de NIF e Cartão de Cidadão isoladas em funções utilitárias.
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
[debug] [2026-05-04T13:55:29.976Z] ----------------------------------------------------------------------
|
|
||||||
[debug] [2026-05-04T13:55:29.979Z] Command: /Users/230414/GestorCondominio/firebase /Users/230414/.cache/firebase/tools/lib/node_modules/firebase-tools/lib/bin/firebase login:ci --no-localhost
|
|
||||||
[debug] [2026-05-04T13:55:29.980Z] CLI Version: 15.16.0
|
|
||||||
[debug] [2026-05-04T13:55:29.980Z] Platform: darwin
|
|
||||||
[debug] [2026-05-04T13:55:29.980Z] Node Version: v20.18.2
|
|
||||||
[debug] [2026-05-04T13:55:29.980Z] Time: Mon May 04 2026 14:55:29 GMT+0100 (Western European Summer Time)
|
|
||||||
[debug] [2026-05-04T13:55:29.981Z] ----------------------------------------------------------------------
|
|
||||||
[debug]
|
|
||||||
[debug] [2026-05-04T13:55:29.983Z] >>> [apiv2][query] GET https://firebase-public.firebaseio.com/cli.json [none]
|
|
||||||
[warn] ⚠ Authenticating with a `login:ci` token is deprecated and will be removed in a future major version of `firebase-tools`. Instead, use a service account key with `GOOGLE_APPLICATION_CREDENTIALS`: https://cloud.google.com/docs/authentication/getting-started
|
|
||||||
[debug] [2026-05-04T13:55:30.192Z] >>> [apiv2][query] POST https://auth.firebase.tools/attest [none]
|
|
||||||
[debug] [2026-05-04T13:55:30.192Z] >>> [apiv2][body] POST https://auth.firebase.tools/attest {"session_id":"cafcf6ff-e83e-4da0-a399-8b9def24c468"}
|
|
||||||
[debug] [2026-05-04T13:55:30.523Z] <<< [apiv2][status] GET https://firebase-public.firebaseio.com/cli.json 200
|
|
||||||
[debug] [2026-05-04T13:55:30.524Z] <<< [apiv2][body] GET https://firebase-public.firebaseio.com/cli.json {"cloudBuildErrorAfter":1594252800000,"cloudBuildWarnAfter":1590019200000,"defaultNode10After":1594252800000,"minVersion":"3.0.5","node8DeploysDisabledAfter":1613390400000,"node8RuntimeDisabledAfter":1615809600000,"node8WarnAfter":1600128000000}
|
|
||||||
[debug] [2026-05-04T13:55:30.779Z] <<< [apiv2][status] POST https://auth.firebase.tools/attest 200
|
|
||||||
[debug] [2026-05-04T13:55:30.781Z] <<< [apiv2][body] POST https://auth.firebase.tools/attest {"token":"agPVoeFtmlUP5LnMPS5KP1cd1Wed0V3Ul1LtwtnIkkI"}
|
|
||||||
[info]
|
|
||||||
[info] To sign in to the Firebase CLI:
|
|
||||||
[info]
|
|
||||||
[info] 1. Take note of your session ID:
|
|
||||||
[info]
|
|
||||||
[info] CAFCF
|
|
||||||
[info]
|
|
||||||
[info] 2. Visit the URL below on any device and follow the instructions to get your code:
|
|
||||||
[info]
|
|
||||||
[info] https://auth.firebase.tools/login?code_challenge=jlTNpVHm9RYrAYUPY2sw9fjN7JR7j0JMKVbDHB9W_1s&session=cafcf6ff-e83e-4da0-a399-8b9def24c468&attest=agPVoeFtmlUP5LnMPS5KP1cd1Wed0V3Ul1LtwtnIkkI
|
|
||||||
[info]
|
|
||||||
[info] 3. Paste or enter the authorization code below once you have it:
|
|
||||||
[info]
|
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
{
|
{
|
||||||
"hosting": {
|
"hosting": {
|
||||||
|
"target": "condomaster",
|
||||||
"public": ".",
|
"public": ".",
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"firebase.json",
|
"firebase.json",
|
||||||
"**/.*",
|
"**/.*",
|
||||||
"**/node_modules/**"
|
"**/node_modules/**"
|
||||||
],
|
],
|
||||||
|
"rewrites": [
|
||||||
|
{
|
||||||
|
"source": "**",
|
||||||
|
"destination": "/index.html"
|
||||||
|
}
|
||||||
|
],
|
||||||
"headers": [
|
"headers": [
|
||||||
{
|
{
|
||||||
"source": "**/*.@(js|css|png|jpg|jpeg|gif|svg|woff|woff2)",
|
"source": "**/*.@(js|css|png|jpg|jpeg|gif|svg|woff|woff2)",
|
||||||
|
|||||||
396
index.html
396
index.html
@@ -4,7 +4,9 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="theme-color" content="#0f172a">
|
||||||
<title>CondoMaster Pro</title>
|
<title>CondoMaster Pro</title>
|
||||||
|
<link rel="manifest" href="./manifest.json">
|
||||||
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script>
|
<script>
|
||||||
@@ -304,132 +306,7 @@
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const WaitingApprovalView = ({ onLogout }) => {
|
const LoginView = ({ onLogin }) => {
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex items-center justify-center bg-slate-100 dark:bg-dark-bg p-4 font-sans text-center">
|
|
||||||
<div className="bg-white dark:bg-dark-surface p-8 rounded-xl shadow-2xl w-full max-w-md border border-slate-100 dark:border-dark-border">
|
|
||||||
<div className="inline-flex p-4 bg-orange-100 dark:bg-orange-900/30 rounded-full mb-6 text-orange-600 dark:text-orange-400">
|
|
||||||
<AlertCircle size={48} />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-bold text-slate-800 dark:text-white mb-2">Conta Pendente</h2>
|
|
||||||
<p className="text-slate-500 dark:text-gray-400 mb-8">
|
|
||||||
O seu registo foi concluído com sucesso, mas a sua conta aguarda aprovação da administração. Por favor, aguarde.
|
|
||||||
</p>
|
|
||||||
<button onClick={onLogout} className="w-full bg-slate-200 hover:bg-slate-300 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-800 dark:text-white font-bold py-3 rounded-lg transition-colors">
|
|
||||||
Sair
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const RegisterView = ({ onToggleView, onRegister }) => {
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
name: '', dob: '', phone: '', cc: '', nif: '', unit: '', email: '', password: '', confirmPassword: ''
|
|
||||||
});
|
|
||||||
const [error, setError] = useState('');
|
|
||||||
|
|
||||||
const isFormValid = Object.values(formData).every(val => val.trim() !== '') && formData.password === formData.confirmPassword;
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setError('');
|
|
||||||
|
|
||||||
if (!validarNIF(formData.nif)) {
|
|
||||||
setError('NIF inválido. Verifique o número inserido.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!validarDocumento(formData.cc)) {
|
|
||||||
setError('Cartão de Cidadão / BI inválido. Verifique o número inserido.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formData.password !== formData.confirmPassword) {
|
|
||||||
setError('As palavras-passe não coincidem.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = await onRegister(formData);
|
|
||||||
if (!result.success) {
|
|
||||||
setError(result.message || 'Ocorreu um erro ao tentar registar a conta.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = (e) => {
|
|
||||||
setFormData({...formData, [e.target.name]: e.target.value});
|
|
||||||
setError('');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex items-center justify-center bg-slate-100 dark:bg-dark-bg p-4 py-12 transition-colors duration-300 font-sans">
|
|
||||||
<div className="bg-white dark:bg-dark-surface p-8 rounded-xl shadow-2xl w-full max-w-2xl animate-fade-in border border-slate-100 dark:border-dark-border">
|
|
||||||
<div className="text-center mb-8">
|
|
||||||
<h1 className="text-2xl font-bold text-slate-800 dark:text-white">Criar Conta</h1>
|
|
||||||
<p className="text-slate-500 dark:text-gray-400 mt-2">Registo de Novo Morador</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div className="col-span-1 md:col-span-2">
|
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Nome Completo</label>
|
|
||||||
<input type="text" name="name" value={formData.name} onChange={handleChange} required className="w-full px-4 py-2 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" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Data de Nascimento</label>
|
|
||||||
<input type="date" name="dob" value={formData.dob} onChange={handleChange} required className="w-full px-4 py-2 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" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Nº Telemóvel</label>
|
|
||||||
<input type="tel" name="phone" value={formData.phone} onChange={handleChange} required className="w-full px-4 py-2 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" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Cartão de Cidadão</label>
|
|
||||||
<input type="text" name="cc" value={formData.cc} onChange={handleChange} required className="w-full px-4 py-2 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" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">NIF</label>
|
|
||||||
<input type="text" name="nif" value={formData.nif} onChange={handleChange} required className="w-full px-4 py-2 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" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Fração (Ex: 1º Dto)</label>
|
|
||||||
<input type="text" name="unit" value={formData.unit} onChange={handleChange} required className="w-full px-4 py-2 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" />
|
|
||||||
</div>
|
|
||||||
<div className="col-span-1 md:col-span-2">
|
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Email</label>
|
|
||||||
<input type="email" name="email" value={formData.email} onChange={handleChange} required className="w-full px-4 py-2 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" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Palavra-passe</label>
|
|
||||||
<input type="password" name="password" value={formData.password} onChange={handleChange} required className="w-full px-4 py-2 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" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Confirmar Palavra-passe</label>
|
|
||||||
<input type="password" name="confirmPassword" value={formData.confirmPassword} onChange={handleChange} required className="w-full px-4 py-2 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" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="col-span-1 md:col-span-2 p-3 bg-red-50 text-red-600 text-sm rounded-lg flex items-center gap-2">
|
|
||||||
<AlertCircle size={16} />
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="col-span-1 md:col-span-2 mt-4 space-y-4">
|
|
||||||
<button type="submit" disabled={!isFormValid} className="w-full bg-blue-600 disabled:bg-blue-300 hover:bg-blue-700 text-white font-bold py-3 rounded-lg transition-colors">
|
|
||||||
Registar
|
|
||||||
</button>
|
|
||||||
<div className="text-center">
|
|
||||||
<button type="button" onClick={onToggleView} className="text-sm text-blue-600 hover:underline dark:text-blue-400">
|
|
||||||
Já tem conta? Iniciar Sessão
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const LoginView = ({ onLogin, onToggleView }) => {
|
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@@ -487,11 +364,6 @@
|
|||||||
<button type="submit" className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded-lg transition-colors shadow-lg shadow-blue-500/30">
|
<button type="submit" className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded-lg transition-colors shadow-lg shadow-blue-500/30">
|
||||||
Entrar
|
Entrar
|
||||||
</button>
|
</button>
|
||||||
<div className="text-center">
|
|
||||||
<button type="button" onClick={onToggleView} className="text-sm text-blue-600 hover:underline dark:text-blue-400">
|
|
||||||
Ainda não tem conta? Registe-se
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -520,111 +392,7 @@
|
|||||||
const [userStatus, setUserStatus] = useState(() => {
|
const [userStatus, setUserStatus] = useState(() => {
|
||||||
return sessionStorage.getItem('condo_user_status') || 'aprovado';
|
return sessionStorage.getItem('condo_user_status') || 'aprovado';
|
||||||
});
|
});
|
||||||
const [authView, setAuthView] = useState('login');
|
|
||||||
|
|
||||||
const handleRegister = async (data) => {
|
|
||||||
// Verificação na lista atual de moradores para prevenir duplicados locais
|
|
||||||
const emailExists = residents.some(r => r.email && r.email.toLowerCase() === data.email.toLowerCase());
|
|
||||||
if (emailExists) {
|
|
||||||
return { success: false, message: 'Este email já se encontra registado no sistema.' };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const userCredential = await createUserWithEmailAndPassword(auth, data.email, data.password);
|
|
||||||
const userId = userCredential.user.uid;
|
|
||||||
|
|
||||||
await set(ref(db, `condominos/${userId}`), {
|
|
||||||
id: userId,
|
|
||||||
name: data.name,
|
|
||||||
email: data.email,
|
|
||||||
contact: data.phone,
|
|
||||||
dob: data.dob,
|
|
||||||
cc: data.cc,
|
|
||||||
nif: data.nif,
|
|
||||||
role: 'morador',
|
|
||||||
status: 'pendente',
|
|
||||||
unit: data.unit,
|
|
||||||
pending: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
await push(ref(db, `notificacoes/admin`), {
|
|
||||||
timestamp: Date.now(),
|
|
||||||
message: `Novo pedido de registo: ${data.name} (${data.unit}). A aguardar aprovação.`,
|
|
||||||
time: 'Agora',
|
|
||||||
type: 'info',
|
|
||||||
read: false
|
|
||||||
});
|
|
||||||
|
|
||||||
sessionStorage.setItem('condo_auth', 'true');
|
|
||||||
sessionStorage.setItem('condo_role', 'morador');
|
|
||||||
sessionStorage.setItem('condo_user_name', data.name);
|
|
||||||
sessionStorage.setItem('condo_user_id', userId);
|
|
||||||
sessionStorage.setItem('condo_user_status', 'pendente');
|
|
||||||
|
|
||||||
setIsAuthenticated(true);
|
|
||||||
setUserRole('morador');
|
|
||||||
setCurrentUserName(data.name);
|
|
||||||
setCurrentUserId(userId);
|
|
||||||
setUserStatus('pendente');
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erro no registo Firebase:", error);
|
|
||||||
|
|
||||||
if (error.code === 'auth/email-already-in-use') {
|
|
||||||
return { success: false, message: 'Este email já está associado a outra conta.' };
|
|
||||||
}
|
|
||||||
if (error.code === 'auth/weak-password') {
|
|
||||||
return { success: false, message: 'A palavra-passe deve ter pelo menos 6 caracteres.' };
|
|
||||||
}
|
|
||||||
if (error.code === 'auth/invalid-email') {
|
|
||||||
return { success: false, message: 'O formato do email é inválido.' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Se falhar devido a falta de configuração, simula registo local (fallback)
|
|
||||||
console.log("A executar fallback local de registo...");
|
|
||||||
const localId = 'local_' + Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await set(ref(db, `condominos/${localId}`), {
|
|
||||||
id: localId,
|
|
||||||
name: data.name,
|
|
||||||
email: data.email,
|
|
||||||
contact: data.phone,
|
|
||||||
dob: data.dob,
|
|
||||||
cc: data.cc,
|
|
||||||
nif: data.nif,
|
|
||||||
role: 'morador',
|
|
||||||
status: 'pendente',
|
|
||||||
unit: data.unit,
|
|
||||||
pending: 0,
|
|
||||||
contact: data.password // Guardado no campo contact apenas para o fallback mock local de login funcionar
|
|
||||||
});
|
|
||||||
|
|
||||||
await push(ref(db, `notificacoes/admin`), {
|
|
||||||
timestamp: Date.now(),
|
|
||||||
message: `Novo pedido de registo: ${data.name} (${data.unit}). A aguardar aprovação.`,
|
|
||||||
time: 'Agora',
|
|
||||||
type: 'info',
|
|
||||||
read: false
|
|
||||||
});
|
|
||||||
} catch(dbErr) {
|
|
||||||
console.error("Base de dados inacessível no fallback.", dbErr);
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionStorage.setItem('condo_auth', 'true');
|
|
||||||
sessionStorage.setItem('condo_role', 'morador');
|
|
||||||
sessionStorage.setItem('condo_user_name', data.name);
|
|
||||||
sessionStorage.setItem('condo_user_id', localId);
|
|
||||||
sessionStorage.setItem('condo_user_status', 'pendente');
|
|
||||||
|
|
||||||
setIsAuthenticated(true);
|
|
||||||
setUserRole('morador');
|
|
||||||
setCurrentUserName(data.name);
|
|
||||||
setCurrentUserId(localId);
|
|
||||||
setUserStatus('pendente');
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogin = async (email, password) => {
|
const handleLogin = async (email, password) => {
|
||||||
try {
|
try {
|
||||||
@@ -723,6 +491,10 @@
|
|||||||
const [messages, setMessages] = useState([]);
|
const [messages, setMessages] = useState([]);
|
||||||
const [newMessageText, setNewMessageText] = useState('');
|
const [newMessageText, setNewMessageText] = useState('');
|
||||||
const [activeChat, setActiveChat] = useState({ type: 'global', id: 'global', name: 'Fórum do Condomínio' });
|
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(() => {
|
useEffect(() => {
|
||||||
const loadData = (path, setter, sortFunc = null) => {
|
const loadData = (path, setter, sortFunc = null) => {
|
||||||
@@ -744,6 +516,7 @@
|
|||||||
const unsubBookings = loadData('reservas', setBookings, (a,b) => new Date(a.date) - new Date(b.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 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 unsubFaturas = loadData('faturas', setFaturas, (a,b) => new Date(b.dataVencimento) - new Date(a.dataVencimento));
|
||||||
|
const unsubGroups = loadData('grupos_chat', setChatGroups);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubResidents();
|
unsubResidents();
|
||||||
@@ -752,6 +525,7 @@
|
|||||||
unsubBookings();
|
unsubBookings();
|
||||||
unsubInvoices();
|
unsubInvoices();
|
||||||
unsubFaturas();
|
unsubFaturas();
|
||||||
|
unsubGroups();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -782,6 +556,8 @@
|
|||||||
let path = 'mural_mensagens';
|
let path = 'mural_mensagens';
|
||||||
if (activeChat.type === 'private') {
|
if (activeChat.type === 'private') {
|
||||||
path = `mensagens_privadas/${[currentUserId, activeChat.id].sort().join('_')}`;
|
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 unsub = onValue(ref(db, path), (snapshot) => {
|
||||||
@@ -1627,13 +1403,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return authView === 'login'
|
return <LoginView onLogin={handleLogin} />;
|
||||||
? <LoginView onLogin={handleLogin} onToggleView={() => setAuthView('register')} />
|
|
||||||
: <RegisterView onRegister={handleRegister} onToggleView={() => setAuthView('login')} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userStatus === 'pendente') {
|
|
||||||
return <WaitingApprovalView onLogout={handleLogout} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1665,7 +1435,8 @@
|
|||||||
{userRole === 'admin' && <SidebarItem icon={Users} label="Condóminos" active={activeTab === 'residents'} onClick={() => { setActiveTab('residents'); setSidebarOpen(false); }} />}
|
{userRole === 'admin' && <SidebarItem icon={Users} label="Condóminos" active={activeTab === 'residents'} onClick={() => { setActiveTab('residents'); setSidebarOpen(false); }} />}
|
||||||
{userRole === 'admin' && <SidebarItem icon={Wallet} label="Finanças" active={activeTab === 'finance'} onClick={() => { setActiveTab('finance'); setSidebarOpen(false); }} />}
|
{userRole === 'admin' && <SidebarItem icon={Wallet} label="Finanças" active={activeTab === 'finance'} onClick={() => { setActiveTab('finance'); setSidebarOpen(false); }} />}
|
||||||
{userRole === 'admin' && <SidebarItem icon={FileText} label="Faturação" active={activeTab === 'billing'} onClick={() => { setActiveTab('billing'); setSidebarOpen(false); }} />}
|
{userRole === 'admin' && <SidebarItem icon={FileText} label="Faturação" active={activeTab === 'billing'} onClick={() => { setActiveTab('billing'); setSidebarOpen(false); }} />}
|
||||||
{userRole === 'admin' && <SidebarItem icon={Users} label="Aprovações" active={activeTab === 'approvals'} onClick={() => { setActiveTab('approvals'); setSidebarOpen(false); }} />}
|
{userRole === 'admin' && <SidebarItem icon={CheckCircle} label="Pagamentos" active={activeTab === 'approvals'} onClick={() => { setActiveTab('approvals'); setSidebarOpen(false); }} />}
|
||||||
|
|
||||||
{userRole === 'morador' && <SidebarItem icon={Wallet} label="Minhas Contas" active={activeTab === 'minhas_contas'} onClick={() => { setActiveTab('minhas_contas'); setSidebarOpen(false); }} />}
|
{userRole === 'morador' && <SidebarItem icon={Wallet} label="Minhas Contas" active={activeTab === 'minhas_contas'} onClick={() => { setActiveTab('minhas_contas'); setSidebarOpen(false); }} />}
|
||||||
<SidebarItem icon={Wrench} label="Manutenção" active={activeTab === 'maintenance'} onClick={() => { setActiveTab('maintenance'); setSidebarOpen(false); }} />
|
<SidebarItem icon={Wrench} label="Manutenção" active={activeTab === 'maintenance'} onClick={() => { setActiveTab('maintenance'); setSidebarOpen(false); }} />
|
||||||
<SidebarItem icon={MessageCircle} label="Mensagens" active={activeTab === 'messages'} onClick={() => { setActiveTab('messages'); setSidebarOpen(false); }} />
|
<SidebarItem icon={MessageCircle} label="Mensagens" active={activeTab === 'messages'} onClick={() => { setActiveTab('messages'); setSidebarOpen(false); }} />
|
||||||
@@ -1864,72 +1635,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* --- APPROVALS --- */}
|
{/* --- APPROVALS --- */}
|
||||||
{activeTab === 'approvals' && userRole === 'admin' && (
|
{activeTab === 'approvals' && userRole === 'admin' && (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h2 className="text-2xl font-bold text-slate-800 dark:text-white">Aprovações de Moradores</h2>
|
|
||||||
<p className="text-slate-500 dark:text-dark-mute">Valide ou rejeite os novos pedidos de registo na plataforma.</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border overflow-hidden">
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="w-full text-left border-collapse">
|
|
||||||
<thead>
|
|
||||||
<tr className="bg-slate-50 dark:bg-dark-bg border-b border-slate-100 dark:border-dark-border text-sm font-semibold text-slate-500 dark:text-slate-400">
|
|
||||||
<th className="p-4">Morador</th>
|
|
||||||
<th className="p-4">NIF / CC</th>
|
|
||||||
<th className="p-4">Contacto</th>
|
|
||||||
<th className="p-4">Data Nasc.</th>
|
|
||||||
<th className="p-4 text-center">Ações</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{residents.filter(r => r.status === 'pendente').map(req => (
|
|
||||||
<tr key={req.id} className="border-b border-slate-50 dark:border-dark-border hover:bg-slate-50/50 dark:hover:bg-dark-bg/50">
|
|
||||||
<td className="p-4">
|
|
||||||
<p className="font-semibold text-slate-700 dark:text-slate-200">{req.name}</p>
|
|
||||||
<p className="text-xs text-slate-400">{req.email}</p>
|
|
||||||
</td>
|
|
||||||
<td className="p-4 text-slate-600 dark:text-slate-400">
|
|
||||||
<p className="text-sm">NIF: {req.nif}</p>
|
|
||||||
<p className="text-sm">CC: {req.cc}</p>
|
|
||||||
</td>
|
|
||||||
<td className="p-4 text-sm text-slate-600 dark:text-slate-400">{req.contact}</td>
|
|
||||||
<td className="p-4 text-sm text-slate-600 dark:text-slate-400">{req.dob}</td>
|
|
||||||
<td className="p-4">
|
|
||||||
<div className="flex justify-center gap-2">
|
|
||||||
<button onClick={() => {
|
|
||||||
if(window.confirm('Aprovar este morador?')) {
|
|
||||||
set(ref(db, `condominos/${req.id}/status`), 'aprovado');
|
|
||||||
sendSystemNotification(`O registo do morador ${req.name} (${req.unit || ''}) foi aprovado.`, 'success', 'admin');
|
|
||||||
sendSystemNotification(`A sua conta foi aprovada pela administração! Bem-vindo(a).`, 'success', req.id);
|
|
||||||
showNotification('Morador aprovado com sucesso!', 'success');
|
|
||||||
}
|
|
||||||
}} 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">
|
|
||||||
<CheckCircle size={18} />
|
|
||||||
</button>
|
|
||||||
<button onClick={() => {
|
|
||||||
if(window.confirm('Rejeitar este pedido? O registo será eliminado.')) {
|
|
||||||
remove(ref(db, `condominos/${req.id}`));
|
|
||||||
showNotification('Registo eliminado.', 'success');
|
|
||||||
}
|
|
||||||
}} 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">
|
|
||||||
<Trash2 size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
{residents.filter(r => r.status === 'pendente').length === 0 && (
|
|
||||||
<tr><td colSpan="5" className="p-8 text-center text-slate-500">Nenhum pedido pendente de aprovação.</td></tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-8 mb-6">
|
|
||||||
<h2 className="text-2xl font-bold text-slate-800 dark:text-white">Aprovações de Pagamentos</h2>
|
<h2 className="text-2xl font-bold text-slate-800 dark:text-white">Aprovações de Pagamentos</h2>
|
||||||
<p className="text-slate-500 dark:text-dark-mute">Valide ou rejeite pagamentos de faturas enviados pelos condóminos.</p>
|
<p className="text-slate-500 dark:text-dark-mute">Valide ou rejeite pagamentos de faturas enviados pelos condóminos.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -2317,7 +2026,7 @@
|
|||||||
<div className="w-full md:w-1/3 border-r border-slate-100 dark:border-dark-border flex flex-col h-full">
|
<div className="w-full md:w-1/3 border-r border-slate-100 dark:border-dark-border flex flex-col h-full">
|
||||||
<div className="p-4 border-b border-slate-100 dark:border-dark-border flex justify-between items-center bg-slate-50 dark:bg-dark-bg">
|
<div className="p-4 border-b border-slate-100 dark:border-dark-border flex justify-between items-center bg-slate-50 dark:bg-dark-bg">
|
||||||
<h3 className="font-bold text-slate-800 dark:text-white">Conversas</h3>
|
<h3 className="font-bold text-slate-800 dark:text-white">Conversas</h3>
|
||||||
<button onClick={() => showNotification('Criação de novos grupos em breve!', 'warning')} 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">
|
<button onClick={() => { 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">
|
||||||
<Plus size={18} />
|
<Plus size={18} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -2345,6 +2054,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{chatGroups.filter(g => g.members && (Object.values(g.members).map(String).includes(String(currentUserId)) || userRole === 'admin')).map(group => (
|
||||||
|
<div
|
||||||
|
key={group.id}
|
||||||
|
onClick={() => 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'}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center text-purple-600 dark:text-purple-400 font-bold shrink-0 text-sm">
|
||||||
|
<Users size={16} />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex justify-between items-baseline mb-0.5">
|
||||||
|
<h4 className="text-sm font-medium text-slate-700 dark:text-slate-300 truncate">{group.name}</h4>
|
||||||
|
{activeChat.id === group.id && <span className="w-2 h-2 rounded-full bg-blue-500"></span>}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-500 dark:text-slate-500 truncate">Grupo</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
{residents.filter(r => r.id !== currentUserId).map(res => (
|
{residents.filter(r => r.id !== currentUserId).map(res => (
|
||||||
<div
|
<div
|
||||||
key={res.id}
|
key={res.id}
|
||||||
@@ -2372,12 +2101,12 @@
|
|||||||
<div className="flex-1 flex flex-col h-full bg-slate-50/50 dark:bg-dark-bg/50">
|
<div className="flex-1 flex flex-col h-full bg-slate-50/50 dark:bg-dark-bg/50">
|
||||||
<div className="p-4 border-b border-slate-100 dark:border-dark-border bg-white dark:bg-dark-surface flex items-center justify-between">
|
<div className="p-4 border-b border-slate-100 dark:border-dark-border bg-white dark:bg-dark-surface flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-bold ${activeChat.type === 'global' ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400' : 'bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-300'}`}>
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-bold ${activeChat.type === 'global' ? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400' : activeChat.type === 'group' ? 'bg-purple-100 dark:bg-purple-900 text-purple-600 dark:text-purple-400' : 'bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-300'}`}>
|
||||||
{activeChat.type === 'global' ? <Users size={20} /> : activeChat.name.substring(0, 2).toUpperCase()}
|
{activeChat.type === 'global' || activeChat.type === 'group' ? <Users size={20} /> : activeChat.name.substring(0, 2).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-bold text-slate-800 dark:text-white">{activeChat.name}</h3>
|
<h3 className="font-bold text-slate-800 dark:text-white">{activeChat.name}</h3>
|
||||||
<p className="text-xs text-green-500 font-medium">{activeChat.type === 'global' ? 'Todos os moradores' : 'Privado'}</p>
|
<p className="text-xs text-green-500 font-medium">{activeChat.type === 'global' ? 'Todos os moradores' : activeChat.type === 'group' ? 'Grupo Privado' : 'Privado'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200"><Info size={20}/></button>
|
<button className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200"><Info size={20}/></button>
|
||||||
@@ -2414,6 +2143,8 @@
|
|||||||
try {
|
try {
|
||||||
const path = activeChat.type === 'global'
|
const path = activeChat.type === 'global'
|
||||||
? 'mural_mensagens'
|
? 'mural_mensagens'
|
||||||
|
: activeChat.type === 'group'
|
||||||
|
? `mensagens_grupo/${activeChat.id}`
|
||||||
: `mensagens_privadas/${[currentUserId, activeChat.id].sort().join('_')}`;
|
: `mensagens_privadas/${[currentUserId, activeChat.id].sort().join('_')}`;
|
||||||
|
|
||||||
const newMsgRef = push(ref(db, path));
|
const newMsgRef = push(ref(db, path));
|
||||||
@@ -2566,6 +2297,53 @@
|
|||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal isOpen={isCreateGroupModalOpen} onClose={() => setIsCreateGroupModalOpen(false)} title="Criar Novo Grupo">
|
||||||
|
<form onSubmit={async (e) => {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<InputGroup label="Nome do Grupo" name="newGroupName" value={newGroupName} onChange={(e) => setNewGroupName(e.target.value)} required />
|
||||||
|
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1 mt-4">Selecionar Moradores</label>
|
||||||
|
<div className="max-h-48 overflow-y-auto border border-slate-300 dark:border-dark-border rounded-lg bg-white dark:bg-dark-card p-2">
|
||||||
|
{residents.filter(r => r.id !== currentUserId).map(r => (
|
||||||
|
<div key={r.id} className="flex items-center gap-2 p-2 hover:bg-slate-50 dark:hover:bg-slate-800 rounded">
|
||||||
|
<input type="checkbox" id={`chk_${r.id}`} checked={newGroupMembers.includes(r.id)}
|
||||||
|
onChange={(e) => {
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<label htmlFor={`chk_${r.id}`} className="text-sm text-slate-700 dark:text-slate-300 cursor-pointer select-none">{r.name} {r.unit ? `(${r.unit})` : ''}</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button type="submit" className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded-lg flex justify-center gap-2 mt-4 transition-colors">
|
||||||
|
<CheckCircle size={20} /> Criar Grupo
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user