correção.geral

This commit is contained in:
2026-05-05 17:16:02 +01:00
parent 7b4504f49c
commit 000c1cd721
6 changed files with 275 additions and 341 deletions

96
.firebase/hosting..cache Normal file
View 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

View File

@@ -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
View 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.

View File

@@ -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]

View File

@@ -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)",

View File

@@ -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"> 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">
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>
); );