Files
GestorCondominio/index.html
2026-06-11 17:06:13 +01:00

4089 lines
279 KiB
HTML

<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#0f172a">
<title>MyCondominium</title>
<link rel="manifest" href="./manifest.json">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
dark: {
bg: '#0f172a',
surface: '#1e293b',
border: '#334155',
card: '#1e293b',
mute: '#94a3b8'
}
}
}
}
}
</script>
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18.2.0",
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
"lucide-react": "https://esm.sh/lucide-react@0.292.0"
}
}
</script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.animate-fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
.animate-slide-up {
animation: slideUp 0.4s ease-out forwards;
}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel" data-type="module">
import React, { useState, useEffect, useRef } from 'react';
import { createRoot } from 'react-dom/client';
import {
Building2, Users, Wallet, Wrench, Bell, Search, Plus, Menu, X,
TrendingUp, TrendingDown, CheckCircle, AlertCircle, Clock, LogOut,
Edit2, Trash2, Save, Filter, MoreVertical, FileText,
Dumbbell, PartyPopper, Trophy, Map, Calendar, MapPin, Info,
MessageCircle, Paperclip, Send, Store, HeartPulse, Waves, ShoppingCart, Navigation, Car, Home, Anchor, Fuel
} from 'lucide-react';
import { app, db } from './firebase.js';
import { getAuth, signInWithEmailAndPassword, createUserWithEmailAndPassword } from 'https://www.gstatic.com/firebasejs/12.1.0/firebase-auth.js';
import { ref, push, set, onValue, onChildAdded, onChildChanged, onChildRemoved, remove, update, get } from 'https://www.gstatic.com/firebasejs/12.1.0/firebase-database.js';
const translations = {
"pt": {
"acesso": "Acesso",
"adicionar": "Adicionar",
"agenda_de_reservas": "Agenda de Reservas",
"alertas_por_email": "Alertas por Email",
"alterar": "Alterar",
"apar_ncia": "Aparência",
"as_minhas_quotas": "As Minhas Quotas",
"ativar_agora": "Ativar Agora",
"atualizar_seguran_a": "Atualizar Segurança",
"avisos_de_cobran_a": "Avisos de Cobrança",
"a_es": "Ações",
"balan_o_l_quido": "Balanço Líquido",
"cancelar": "Cancelar",
"categoria": "Categoria",
"claro": "Claro",
"com_rcio": "Comércio",
"cond_mino": "Condómino",
"cond_minos": "Condóminos",
"confirmar": "Confirmar",
"confirmar_nova_palavra_passe": "Confirmar Nova Palavra-passe",
"confirmar_reserva": "Confirmar Reserva",
"confirma_o": "Confirmação",
"consulte_as_suas_despesas_e_faturas_emitidas": "Consulte as suas despesas e faturas emitidas",
"contacto": "Contacto",
"conversas": "Conversas",
"criar_grupo": "Criar Grupo",
"criar_novo_grupo": "Criar Novo Grupo",
"dados_pessoais": "Dados Pessoais",
"dashboard": "Dashboard",
"data": "Data",
"data_emiss_o": "Data Emissão",
"data_vencimento": "Data Vencimento",
"descarregar_recibo": "Descarregar Recibo",
"descri_o": "Descrição",
"detalhes_da_rota": "Detalhes da Rota",
"detalhes_do_movimento": "Detalhes do movimento",
"detalhes_e_navega_o": "Detalhes e Navegação",
"di_rio_financeiro": "Diário Financeiro",
"editar": "Editar",
"eliminar": "Eliminar",
"em_d_vida": "Em Dívida",
"em_resolu_o": "Em resolução",
"email": "Email",
"emita_faturas_ou_avise_cond_minos_individualmente": "Emita faturas ou avise condóminos individualmente",
"emitir_fatura": "Emitir Fatura",
"emitir_nova_fatura": "Emitir Nova Fatura",
"endere_o_de_email": "Endereço de email",
"enviar_por_email": "Enviar por Email",
"escuro": "Escuro",
"espa_o": "Espaço",
"espa_os_comuns": "Espaços Comuns",
"estado": "Estado",
"estado_quotas": "Estado Quotas",
"ex_14_00_16_00": "Ex: 14:00 - 16:00",
"ex_1_esq": "Ex: 1º Esq",
"ex_hall_de_entrada": "Ex: Hall de entrada",
"ex_limpeza_elevadores_quotas": "Ex: Limpeza, Elevadores, Quotas...",
"ex_l_mpada_fundida": "Ex: Lâmpada fundida",
"explore_e_encontre_rotas_no_condom_nio": "Explore e encontre rotas no condomínio",
"fatura": "Fatura",
"faturar_na_hora": "Faturar na Hora",
"fatura_o": "Faturação",
"fazer_reserva": "Fazer Reserva",
"finan_as": "Finanças",
"fra_o": "Fração",
"f_rum_do_condom_nio": "Fórum do Condomínio",
"geral": "Geral",
"gest_o_de_cond_minos": "Gestão de Condóminos",
"gest_o_de_pedidos_e_repara_es": "Gestão de pedidos e reparações",
"gin_sio": "Ginásio",
"gin_sio_privado": "Ginásio Privado",
"grupo": "Grupo",
"grupo_partilhado": "Grupo partilhado",
"guardar_altera_es": "Guardar Alterações",
"guardar_cond_mino": "Guardar Condómino",
"hist_rico_de_reservas": "Histórico de Reservas",
"hor_rio": "Horário",
"hor_rio_08_00_22_00": "Horário: 08:00 - 22:00",
"idioma_da_aplica_o": "Idioma da Aplicação",
"lazer": "Lazer",
"limpar": "Limpar",
"lista_completa_de_agendamentos_em_todos_os_espa_os_de_lazer": "Lista completa de agendamentos em todos os espaços de lazer",
"localiza_o": "Localização",
"manuten_o": "Manutenção",
"manuten_o_e_ocorr_ncias": "Manutenção e Ocorrências",
"manuten_es_ativas": "Manutenções Ativas",
"mapa": "Mapa",
"marcar_como_lida": "Marcar como lida",
"mensagens": "Mensagens",
"meu_perfil": "Meu Perfil",
"min_8_caracteres": "Min. 8 caracteres",
"minhas_contas": "Minhas Contas",
"minhas_faturas": "Minhas Faturas",
"morador": "Morador",
"mudar_permiss_es": "Mudar Permissões",
"mycondominium": "MyCondominium",
"navega_o_inteligente": "Navegação Inteligente",
"nome_completo": "Nome Completo",
"nome_do_grupo": "Nome do Grupo",
"nome_do_propriet_rio": "Nome do proprietário",
"nome_do_residente": "Nome do residente",
"notificar": "Notificar",
"notifica_es": "Notificações",
"notifica_es_push_no_navegador": "Notificações Push no Navegador",
"nova_palavra_passe": "Nova Palavra-passe",
"nova_reserva": "Nova Reserva",
"novo_registo": "Novo Registo",
"n_vel_de_acesso": "Nível de Acesso",
"pagamentos": "Pagamentos",
"pagamentos_conclu_dos": "Pagamentos Concluídos",
"pagar": "Pagar",
"pago": "Pago",
"palavra_passe": "Palavra-passe",
"palavra_passe_atual": "Palavra-passe Atual",
"parque_jogos": "Parque Jogos",
"parque_de_jogos": "Parque de Jogos",
"permiss_es": "Permissões",
"portal_de_gest_o": "Portal de Gestão",
"prefer_ncias": "Preferências",
"prefer_ncias_da_aplica_o": "Preferências da Aplicação",
"prioridade": "Prioridade",
"propriet_rio": "Proprietário",
"pr_ximas_reservas": "Próximas Reservas",
"quadro_de_avisos": "Quadro de Avisos",
"quotas_em_atraso": "Quotas em Atraso",
"rio_tejo": "RIO TEJO",
"recarregar_p_gina": "Recarregar Página",
"recibo": "Recibo",
"registar_movimento": "Registar Movimento",
"registar_movimento_financeiro": "Registar Movimento Financeiro",
"relat_rios_semanais_autom_ticos": "Relatórios Semanais Automáticos",
"reportar": "Reportar",
"reportar_ocorr_ncia": "Reportar Ocorrência",
"reportar_problema": "Reportar Problema",
"reservado_para_cond_mino": "Reservado para (Condómino)",
"reservar_agora": "Reservar Agora",
"reservas_m_s": "Reservas (Mês)",
"residencial": "Residencial",
"resolver": "Resolver",
"resolver_problemas": "Resolver Problemas",
"restaurar_base_de_dados": "Restaurar Base de Dados",
"saldo_dispon_vel": "Saldo Disponível",
"sal_o_de_festas": "Salão de Festas",
"seguran_a": "Segurança",
"selecionar_moradores": "Selecionar Moradores",
"sem_novas_notifica_es": "Sem novas notificações",
"sem_reservas": "Sem reservas",
"sem_valores_pendentes": "Sem valores pendentes",
"senha_de_acesso": "Senha de acesso",
"servi_os": "Serviços",
"sistema": "Sistema",
"telefone": "Telefone",
"telem_vel": "Telemóvel",
"terminar_sess_o": "Terminar Sessão",
"tipo": "Tipo",
"total_pago": "Total Pago",
"total_pendente": "Total Pendente",
"total_agendado": "Total agendado",
"t_tulo_do_problema": "Título do Problema",
"via_central": "VIA CENTRAL",
"valor": "Valor (€)",
"valor_pendente": "Valor Pendente (€)",
"vencimento": "Vencimento",
"ver_todas_as_reservas": "Ver todas as Reservas",
"em_progresso": "Em Progresso",
"receita": "Receita",
"despesa": "Despesa",
"em_valida_o": "Em Validação",
"alta": "Alta",
"baixa": "Baixa",
"novo": "Novo",
"resolvido": "Resolvido",
"pendente": "Pendente",
"confirmado": "Confirmado",
"m_dia": "Média",
"atrasado": "Atrasado",
"ana_silva": "Ana Silva",
"carlos_santos": "Carlos Santos",
"maria_pereira": "Maria Pereira",
"jo_o_ferreira": "João Ferreira",
"sofia_costa": "Sofia Costa",
"administra_o": "Administração",
"utilizador": "Utilizador",
"pesquisar_cond_mino_por_nome_ou_fra_o": "Pesquisar condómino por nome ou fração...",
"pesquisar_transa_o": "Pesquisar transação...",
"pesquisar_na_lista": "Pesquisar na lista...",
"pesquisar_fatura": "Pesquisar fatura...",
"escreva_a_sua_mensagem": "Escreva a sua mensagem...",
"pesquisar_reserva": "Pesquisar reserva...",
"vs_m_s_passado": "vs. mês passado",
"algo_correu_mal_erro_na_aplica_o": "Algo correu mal (Erro na Aplicação)",
"email_ou_palavra_passe_incorreta": "Email ou Palavra-passe incorreta",
"a_entrar": "A entrar...",
"entrar": "Entrar",
"nenhuma_ocorr_ncia_encontrada": "Nenhuma ocorrência encontrada.",
"nenhuma_transa_o_financeira_encontrada": "Nenhuma transação financeira encontrada.",
"nenhum_cond_mino_encontrado": "Nenhum condómino encontrado.",
"nenhuma_fatura_encontrada": "Nenhuma fatura encontrada.",
"nenhuma_reserva_encontrada": "Nenhuma reserva encontrada.",
"tem_a_certeza_que_deseja_terminar_sess_o": "Tem a certeza que deseja terminar sessão?",
"permiss_es_de_utilizador_atualizadas": "Permissões de utilizador atualizadas",
"erro_ao_atualizar_permiss_o": "Erro ao atualizar permissão.",
"morador_atualizado_com_sucesso": "Morador atualizado com sucesso",
"morador_adicionado_com_sucesso": "Morador adicionado com sucesso",
"erro_ao_guardar_morador": "Erro ao guardar morador",
"ocorr_ncia_resolvida_com_sucesso": "Ocorrência resolvida com sucesso",
"ocorr_ncia_atualizada": "Ocorrência atualizada",
"transa_o_adicionada_com_sucesso": "Transação adicionada com sucesso",
"erro_ao_guardar_transa_o": "Erro ao guardar transação",
"fatura_emitida_com_sucesso": "Fatura emitida com sucesso",
"erro_ao_emitir_fatura": "Erro ao emitir fatura",
"fatura_atualizada_para_paga": "Fatura atualizada para Paga",
"erro_ao_atualizar_fatura": "Erro ao atualizar fatura",
"reserva_efetuada_com_sucesso": "Reserva efetuada com sucesso",
"reserva_atualizada_com_sucesso": "Reserva atualizada com sucesso",
"erro_ao_fazer_reserva": "Erro ao fazer reserva"
},
"en": {
"acesso": "Access",
"adicionar": "Add",
"agenda_de_reservas": "Booking Schedule",
"alertas_por_email": "Email Alerts",
"alterar": "Change",
"apar_ncia": "Appearance",
"as_minhas_quotas": "My Dues",
"ativar_agora": "Activate Now",
"atualizar_seguran_a": "Update Security",
"avisos_de_cobran_a": "Billing Notices",
"a_es": "Actions",
"balan_o_l_quido": "Net Balance",
"cancelar": "Cancel",
"categoria": "Category",
"claro": "Light",
"com_rcio": "Commerce",
"cond_mino": "Resident",
"cond_minos": "Residents",
"confirmar": "Confirm",
"confirmar_nova_palavra_passe": "Confirm New Password",
"confirmar_reserva": "Confirm Booking",
"confirma_o": "Confirmation",
"consulte_as_suas_despesas_e_faturas_emitidas": "Check your expenses and issued invoices",
"contacto": "Contact",
"conversas": "Conversations",
"criar_grupo": "Create Group",
"criar_novo_grupo": "Create New Group",
"dados_pessoais": "Personal Data",
"dashboard": "Dashboard",
"data": "Date",
"data_emiss_o": "Issue Date",
"data_vencimento": "Due Date",
"descarregar_recibo": "Download Receipt",
"descri_o": "Description",
"detalhes_da_rota": "Route Details",
"detalhes_do_movimento": "Movement details",
"detalhes_e_navega_o": "Details and Navigation",
"di_rio_financeiro": "Financial Diary",
"editar": "Edit",
"eliminar": "Delete",
"em_d_vida": "In Debt",
"em_resolu_o": "In Resolution",
"email": "Email",
"emita_faturas_ou_avise_cond_minos_individualmente": "Issue invoices or notify residents individually",
"emitir_fatura": "Issue Invoice",
"emitir_nova_fatura": "Issue New Invoice",
"endere_o_de_email": "Email Address",
"enviar_por_email": "Send by Email",
"escuro": "Dark",
"espa_o": "Space",
"espa_os_comuns": "Common Spaces",
"estado": "Status",
"estado_quotas": "Dues Status",
"ex_14_00_16_00": "E.g.: 14:00 - 16:00",
"ex_1_esq": "E.g.: 1st Left",
"ex_hall_de_entrada": "E.g.: Entrance Hall",
"ex_limpeza_elevadores_quotas": "E.g.: Cleaning, Elevators, Dues...",
"ex_l_mpada_fundida": "E.g.: Blown bulb",
"explore_e_encontre_rotas_no_condom_nio": "Explore and find routes in the condo",
"fatura": "Invoice",
"faturar_na_hora": "Invoice Now",
"fatura_o": "Billing",
"fazer_reserva": "Make Booking",
"finan_as": "Finances",
"fra_o": "Unit",
"f_rum_do_condom_nio": "Condo Forum",
"geral": "General",
"gest_o_de_cond_minos": "Residents Management",
"gest_o_de_pedidos_e_repara_es": "Requests and repairs management",
"gin_sio": "Gym",
"gin_sio_privado": "Private Gym",
"grupo": "Group",
"grupo_partilhado": "Shared group",
"guardar_altera_es": "Save Changes",
"guardar_cond_mino": "Save Resident",
"hist_rico_de_reservas": "Booking History",
"hor_rio": "Schedule",
"hor_rio_08_00_22_00": "Hours: 08:00 - 22:00",
"idioma_da_aplica_o": "App Language",
"lazer": "Leisure",
"limpar": "Clear",
"lista_completa_de_agendamentos_em_todos_os_espa_os_de_lazer": "Complete list of bookings in all leisure spaces",
"localiza_o": "Location",
"manuten_o": "Maintenance",
"manuten_o_e_ocorr_ncias": "Maintenance and Issues",
"manuten_es_ativas": "Active Maintenance",
"mapa": "Map",
"marcar_como_lida": "Mark as read",
"mensagens": "Messages",
"meu_perfil": "My Profile",
"min_8_caracteres": "Min. 8 characters",
"minhas_contas": "My Accounts",
"minhas_faturas": "My Invoices",
"morador": "Resident",
"mudar_permiss_es": "Change Permissions",
"mycondominium": "MyCondominium",
"navega_o_inteligente": "Smart Navigation",
"nome_completo": "Full Name",
"nome_do_grupo": "Group Name",
"nome_do_propriet_rio": "Owner Name",
"nome_do_residente": "Resident Name",
"notificar": "Notify",
"notifica_es": "Notifications",
"notifica_es_push_no_navegador": "Browser Push Notifications",
"nova_palavra_passe": "New Password",
"nova_reserva": "New Booking",
"novo_registo": "New Registration",
"n_vel_de_acesso": "Access Level",
"pagamentos": "Payments",
"pagamentos_conclu_dos": "Completed Payments",
"pagar": "Pay",
"pago": "Paid",
"palavra_passe": "Password",
"palavra_passe_atual": "Current Password",
"parque_jogos": "Playground",
"parque_de_jogos": "Playground",
"permiss_es": "Permissions",
"portal_de_gest_o": "Management Portal",
"prefer_ncias": "Preferences",
"prefer_ncias_da_aplica_o": "App Preferences",
"prioridade": "Priority",
"propriet_rio": "Owner",
"pr_ximas_reservas": "Upcoming Bookings",
"quadro_de_avisos": "Notice Board",
"quotas_em_atraso": "Overdue Dues",
"rio_tejo": "RIO TEJO",
"recarregar_p_gina": "Reload Page",
"recibo": "Receipt",
"registar_movimento": "Register Movement",
"registar_movimento_financeiro": "Register Financial Movement",
"relat_rios_semanais_autom_ticos": "Automated Weekly Reports",
"reportar": "Report",
"reportar_ocorr_ncia": "Report Issue",
"reportar_problema": "Report Problem",
"reservado_para_cond_mino": "Booked for (Resident)",
"reservar_agora": "Book Now",
"reservas_m_s": "Bookings (Month)",
"residencial": "Residential",
"resolver": "Resolve",
"resolver_problemas": "Solve Problems",
"restaurar_base_de_dados": "Restore Database",
"saldo_dispon_vel": "Available Balance",
"sal_o_de_festas": "Party Room",
"seguran_a": "Security",
"selecionar_moradores": "Select Residents",
"sem_novas_notifica_es": "No new notifications",
"sem_reservas": "No bookings",
"sem_valores_pendentes": "No pending values",
"senha_de_acesso": "Access Password",
"servi_os": "Services",
"sistema": "System",
"telefone": "Phone",
"telem_vel": "Mobile",
"terminar_sess_o": "Log Out",
"tipo": "Type",
"total_pago": "Total Paid",
"total_pendente": "Total Pending",
"total_agendado": "Total Scheduled",
"t_tulo_do_problema": "Problem Title",
"via_central": "VIA CENTRAL",
"valor": "Amount (€)",
"valor_pendente": "Pending Amount (€)",
"vencimento": "Due Date",
"ver_todas_as_reservas": "See all Bookings",
"em_progresso": "In Progress",
"receita": "Income",
"despesa": "Expense",
"em_valida_o": "Validating",
"alta": "High",
"baixa": "Low",
"novo": "New",
"resolvido": "Resolved",
"pendente": "Pending",
"confirmado": "Confirmed",
"m_dia": "Medium",
"atrasado": "Overdue",
"ana_silva": "Ana Silva",
"carlos_santos": "Carlos Santos",
"maria_pereira": "Maria Pereira",
"jo_o_ferreira": "João Ferreira",
"sofia_costa": "Sofia Costa",
"administra_o": "Administration",
"utilizador": "User",
"pesquisar_cond_mino_por_nome_ou_fra_o": "Search resident by name or unit...",
"pesquisar_transa_o": "Search transaction...",
"pesquisar_na_lista": "Search in list...",
"pesquisar_fatura": "Search invoice...",
"escreva_a_sua_mensagem": "Write your message...",
"pesquisar_reserva": "Search booking...",
"vs_m_s_passado": "vs. last month",
"algo_correu_mal_erro_na_aplica_o": "Something went wrong (App Error)",
"email_ou_palavra_passe_incorreta": "Incorrect Email or Password",
"a_entrar": "Logging in...",
"entrar": "Log In",
"nenhuma_ocorr_ncia_encontrada": "No issues found.",
"nenhuma_transa_o_financeira_encontrada": "No financial transactions found.",
"nenhum_cond_mino_encontrado": "No resident found.",
"nenhuma_fatura_encontrada": "No invoice found.",
"nenhuma_reserva_encontrada": "No booking found.",
"tem_a_certeza_que_deseja_terminar_sess_o": "Are you sure you want to log out?",
"permiss_es_de_utilizador_atualizadas": "User permissions updated",
"erro_ao_atualizar_permiss_o": "Error updating permission.",
"morador_atualizado_com_sucesso": "Resident updated successfully",
"morador_adicionado_com_sucesso": "Resident added successfully",
"erro_ao_guardar_morador": "Error saving resident",
"ocorr_ncia_resolvida_com_sucesso": "Issue resolved successfully",
"ocorr_ncia_atualizada": "Issue updated",
"transa_o_adicionada_com_sucesso": "Transaction added successfully",
"erro_ao_guardar_transa_o": "Error saving transaction",
"fatura_emitida_com_sucesso": "Invoice issued successfully",
"erro_ao_emitir_fatura": "Error issuing invoice",
"fatura_atualizada_para_paga": "Invoice updated to Paid",
"erro_ao_atualizar_fatura": "Error updating invoice",
"reserva_efetuada_com_sucesso": "Booking made successfully",
"reserva_atualizada_com_sucesso": "Booking updated successfully",
"erro_ao_fazer_reserva": "Error making booking"
},
"es": {
"acesso": "Acceso",
"adicionar": "Añadir",
"agenda_de_reservas": "Agenda de Reservas",
"alertas_por_email": "Alertas por Email",
"alterar": "Cambiar",
"apar_ncia": "Apariencia",
"as_minhas_quotas": "Mis Cuotas",
"ativar_agora": "Activar Ahora",
"atualizar_seguran_a": "Actualizar Seguridad",
"avisos_de_cobran_a": "Avisos de Cobro",
"a_es": "Acciones",
"balan_o_l_quido": "Balance Neto",
"cancelar": "Cancelar",
"categoria": "Categoría",
"claro": "Claro",
"com_rcio": "Comercio",
"cond_mino": "Residente",
"cond_minos": "Residentes",
"confirmar": "Confirmar",
"confirmar_nova_palavra_passe": "Confirmar Nueva Contraseña",
"confirmar_reserva": "Confirmar Reserva",
"confirma_o": "Confirmación",
"consulte_as_suas_despesas_e_faturas_emitidas": "Consulte sus gastos y facturas emitidas",
"contacto": "Contacto",
"conversas": "Conversaciones",
"criar_grupo": "Crear Grupo",
"criar_novo_grupo": "Crear Nuevo Grupo",
"dados_pessoais": "Datos Personales",
"dashboard": "Panel de Control",
"data": "Fecha",
"data_emiss_o": "Fecha de Emisión",
"data_vencimento": "Fecha de Vencimiento",
"descarregar_recibo": "Descargar Recibo",
"descri_o": "Descripción",
"detalhes_da_rota": "Detalles de la Ruta",
"detalhes_do_movimento": "Detalles del movimiento",
"detalhes_e_navega_o": "Detalles y Navegación",
"di_rio_financeiro": "Diario Financiero",
"editar": "Editar",
"eliminar": "Eliminar",
"em_d_vida": "En Deuda",
"em_resolu_o": "En resolución",
"email": "Correo",
"emita_faturas_ou_avise_cond_minos_individualmente": "Emita facturas o avise a los residentes individualmente",
"emitir_fatura": "Emitir Factura",
"emitir_nova_fatura": "Emitir Nueva Factura",
"endere_o_de_email": "Dirección de correo",
"enviar_por_email": "Enviar por Correo",
"escuro": "Oscuro",
"espa_o": "Espacio",
"espa_os_comuns": "Espacios Comunes",
"estado": "Estado",
"estado_quotas": "Estado de Cuotas",
"ex_14_00_16_00": "Ej: 14:00 - 16:00",
"ex_1_esq": "Ej: 1º Izq",
"ex_hall_de_entrada": "Ej: Hall de entrada",
"ex_limpeza_elevadores_quotas": "Ej: Limpieza, Ascensores, Cuotas...",
"ex_l_mpada_fundida": "Ej: Bombilla fundida",
"explore_e_encontre_rotas_no_condom_nio": "Explore y encuentre rutas en el condominio",
"fatura": "Factura",
"faturar_na_hora": "Facturar al Instante",
"fatura_o": "Facturación",
"fazer_reserva": "Hacer Reserva",
"finan_as": "Finanzas",
"fra_o": "Fracción",
"f_rum_do_condom_nio": "Foro del Condominio",
"geral": "General",
"gest_o_de_cond_minos": "Gestión de Residentes",
"gest_o_de_pedidos_e_repara_es": "Gestión de solicitudes y reparaciones",
"gin_sio": "Gimnasio",
"gin_sio_privado": "Gimnasio Privado",
"grupo": "Grupo",
"grupo_partilhado": "Grupo compartido",
"guardar_altera_es": "Guardar Cambios",
"guardar_cond_mino": "Guardar Residente",
"hist_rico_de_reservas": "Historial de Reservas",
"hor_rio": "Horario",
"hor_rio_08_00_22_00": "Horario: 08:00 - 22:00",
"idioma_da_aplica_o": "Idioma de la Aplicación",
"lazer": "Ocio",
"limpar": "Limpiar",
"lista_completa_de_agendamentos_em_todos_os_espa_os_de_lazer": "Lista completa de reservas en todos los espacios de ocio",
"localiza_o": "Ubicación",
"manuten_o": "Mantenimiento",
"manuten_o_e_ocorr_ncias": "Mantenimiento e Incidencias",
"manuten_es_ativas": "Mantenimientos Activos",
"mapa": "Mapa",
"marcar_como_lida": "Marcar como leída",
"mensagens": "Mensajes",
"meu_perfil": "Mi Perfil",
"min_8_caracteres": "Mín. 8 caracteres",
"minhas_contas": "Mis Cuentas",
"minhas_faturas": "Mis Facturas",
"morador": "Residente",
"mudar_permiss_es": "Cambiar Permisos",
"mycondominium": "MyCondominium",
"navega_o_inteligente": "Navegación Inteligente",
"nome_completo": "Nombre Completo",
"nome_do_grupo": "Nombre del Grupo",
"nome_do_propriet_rio": "Nombre del propietario",
"nome_do_residente": "Nombre del residente",
"notificar": "Notificar",
"notifica_es": "Notificaciones",
"notifica_es_push_no_navegador": "Notificaciones Push en el Navegador",
"nova_palavra_passe": "Nueva Contraseña",
"nova_reserva": "Nueva Reserva",
"novo_registo": "Nuevo Registro",
"n_vel_de_acesso": "Nivel de Acceso",
"pagamentos": "Pagos",
"pagamentos_conclu_dos": "Pagos Completados",
"pagar": "Pagar",
"pago": "Pagado",
"palavra_passe": "Contraseña",
"palavra_passe_atual": "Contraseña Actual",
"parque_jogos": "Parque de Juegos",
"parque_de_jogos": "Parque de Juegos",
"permiss_es": "Permisos",
"portal_de_gest_o": "Portal de Gestión",
"prefer_ncias": "Preferencias",
"prefer_ncias_da_aplica_o": "Preferencias de la Aplicación",
"prioridade": "Prioridad",
"propriet_rio": "Propietario",
"pr_ximas_reservas": "Próximas Reservas",
"quadro_de_avisos": "Tablón de Anuncios",
"quotas_em_atraso": "Cuotas Atrasadas",
"rio_tejo": "RIO TEJO",
"recarregar_p_gina": "Recargar Página",
"recibo": "Recibo",
"registar_movimento": "Registrar Movimiento",
"registar_movimento_financeiro": "Registrar Movimiento Financiero",
"relat_rios_semanais_autom_ticos": "Informes Semanales Automáticos",
"reportar": "Reportar",
"reportar_ocorr_ncia": "Reportar Incidencia",
"reportar_problema": "Reportar Problema",
"reservado_para_cond_mino": "Reservado para (Residente)",
"reservar_agora": "Reservar Ahora",
"reservas_m_s": "Reservas (Mes)",
"residencial": "Residencial",
"resolver": "Resolver",
"resolver_problemas": "Resolver Problemas",
"restaurar_base_de_dados": "Restaurar Base de Datos",
"saldo_dispon_vel": "Saldo Disponible",
"sal_o_de_festas": "Salón de Fiestas",
"seguran_a": "Seguridad",
"selecionar_moradores": "Seleccionar Residentes",
"sem_novas_notifica_es": "No hay nuevas notificaciones",
"sem_reservas": "Sin reservas",
"sem_valores_pendentes": "Sin valores pendientes",
"senha_de_acesso": "Contraseña de acceso",
"servi_os": "Servicios",
"sistema": "Sistema",
"telefone": "Teléfono",
"telem_vel": "Móvil",
"terminar_sess_o": "Cerrar Sesión",
"tipo": "Tipo",
"total_pago": "Total Pagado",
"total_pendente": "Total Pendiente",
"total_agendado": "Total Agendado",
"t_tulo_do_problema": "Título del Problema",
"via_central": "VIA CENTRAL",
"valor": "Valor (€)",
"valor_pendente": "Valor Pendiente (€)",
"vencimento": "Vencimiento",
"ver_todas_as_reservas": "Ver todas las Reservas",
"em_progresso": "En Progreso",
"receita": "Ingreso",
"despesa": "Gasto",
"em_valida_o": "En Validación",
"alta": "Alta",
"baixa": "Baja",
"novo": "Nuevo",
"resolvido": "Resuelto",
"pendente": "Pendiente",
"confirmado": "Confirmado",
"m_dia": "Media",
"atrasado": "Atrasado",
"ana_silva": "Ana Silva",
"carlos_santos": "Carlos Santos",
"maria_pereira": "Maria Pereira",
"jo_o_ferreira": "João Ferreira",
"sofia_costa": "Sofia Costa",
"administra_o": "Administración",
"utilizador": "Usuario",
"pesquisar_cond_mino_por_nome_ou_fra_o": "Buscar residente por nombre o fracción...",
"pesquisar_transa_o": "Buscar transacción...",
"pesquisar_na_lista": "Buscar en la lista...",
"pesquisar_fatura": "Buscar factura...",
"escreva_a_sua_mensagem": "Escriba su mensaje...",
"pesquisar_reserva": "Buscar reserva...",
"vs_m_s_passado": "vs. mes pasado",
"algo_correu_mal_erro_na_aplica_o": "Algo salió mal (Error de Aplicación)",
"email_ou_palavra_passe_incorreta": "Correo o Contraseña incorrecta",
"a_entrar": "Entrando...",
"entrar": "Entrar",
"nenhuma_ocorr_ncia_encontrada": "Ninguna incidencia encontrada.",
"nenhuma_transa_o_financeira_encontrada": "Ninguna transacción financiera encontrada.",
"nenhum_cond_mino_encontrado": "Ningún residente encontrado.",
"nenhuma_fatura_encontrada": "Ninguna factura encontrada.",
"nenhuma_reserva_encontrada": "Ninguna reserva encontrada.",
"tem_a_certeza_que_deseja_terminar_sess_o": "¿Está seguro que desea cerrar sesión?",
"permiss_es_de_utilizador_atualizadas": "Permisos de usuario actualizados",
"erro_ao_atualizar_permiss_o": "Error al actualizar permiso.",
"morador_atualizado_com_sucesso": "Residente actualizado con éxito",
"morador_adicionado_com_sucesso": "Residente añadido con éxito",
"erro_ao_guardar_morador": "Error al guardar residente",
"ocorr_ncia_resolvida_com_sucesso": "Incidencia resuelta con éxito",
"ocorr_ncia_atualizada": "Incidencia actualizada",
"transa_o_adicionada_com_sucesso": "Transacción añadida con éxito",
"erro_ao_guardar_transa_o": "Error al guardar transacción",
"fatura_emitida_com_sucesso": "Factura emitida con éxito",
"erro_ao_emitir_fatura": "Error al emitir factura",
"fatura_atualizada_para_paga": "Factura actualizada a Pagada",
"erro_ao_atualizar_fatura": "Error al actualizar factura",
"reserva_efetuada_com_sucesso": "Reserva efectuada con éxito",
"reserva_atualizada_com_sucesso": "Reserva actualizada con éxito",
"erro_ao_fazer_reserva": "Error al hacer reserva"
},
"fr": {
"acesso": "Accès",
"adicionar": "Ajouter",
"agenda_de_reservas": "Calendrier des Réservations",
"alertas_por_email": "Alertes par Email",
"alterar": "Modifier",
"apar_ncia": "Apparence",
"as_minhas_quotas": "Mes Cotisations",
"ativar_agora": "Activer Maintenant",
"atualizar_seguran_a": "Mettre à jour la Sécurité",
"avisos_de_cobran_a": "Avis de Facturation",
"a_es": "Actions",
"balan_o_l_quido": "Solde Net",
"cancelar": "Annuler",
"categoria": "Catégorie",
"claro": "Clair",
"com_rcio": "Commerce",
"cond_mino": "Résident",
"cond_minos": "Résidents",
"confirmar": "Confirmer",
"confirmar_nova_palavra_passe": "Confirmer le Nouveau Mot de passe",
"confirmar_reserva": "Confirmer la Réservation",
"confirma_o": "Confirmation",
"consulte_as_suas_despesas_e_faturas_emitidas": "Consultez vos dépenses et factures émises",
"contacto": "Contact",
"conversas": "Conversations",
"criar_grupo": "Créer un Groupe",
"criar_novo_grupo": "Créer un Nouveau Groupe",
"dados_pessoais": "Données Personnelles",
"dashboard": "Tableau de Bord",
"data": "Date",
"data_emiss_o": "Date d'Émission",
"data_vencimento": "Date d'Échéance",
"descarregar_recibo": "Télécharger le Reçu",
"descri_o": "Description",
"detalhes_da_rota": "Détails de l'Itinéraire",
"detalhes_do_movimento": "Détails du mouvement",
"detalhes_e_navega_o": "Détails et Navigation",
"di_rio_financeiro": "Journal Financier",
"editar": "Modifier",
"eliminar": "Supprimer",
"em_d_vida": "En Dette",
"em_resolu_o": "En résolution",
"email": "Email",
"emita_faturas_ou_avise_cond_minos_individualmente": "Émettez des factures ou informez les résidents individuellement",
"emitir_fatura": "Émettre une Facture",
"emitir_nova_fatura": "Émettre une Nouvelle Facture",
"endere_o_de_email": "Adresse e-mail",
"enviar_por_email": "Envoyer par Email",
"escuro": "Sombre",
"espa_o": "Espace",
"espa_os_comuns": "Espaces Communs",
"estado": "Statut",
"estado_quotas": "Statut des Cotisations",
"ex_14_00_16_00": "Ex : 14:00 - 16:00",
"ex_1_esq": "Ex : 1er Gauche",
"ex_hall_de_entrada": "Ex : Hall d'entrée",
"ex_limpeza_elevadores_quotas": "Ex : Nettoyage, Ascenseurs, Cotisations...",
"ex_l_mpada_fundida": "Ex : Ampoule grillée",
"explore_e_encontre_rotas_no_condom_nio": "Explorez et trouvez des itinéraires dans la copropriété",
"fatura": "Facture",
"faturar_na_hora": "Facturer Maintenant",
"fatura_o": "Facturation",
"fazer_reserva": "Faire une Réservation",
"finan_as": "Finances",
"fra_o": "Unité",
"f_rum_do_condom_nio": "Forum de la Copropriété",
"geral": "Général",
"gest_o_de_cond_minos": "Gestion des Résidents",
"gest_o_de_pedidos_e_repara_es": "Gestion des demandes et réparations",
"gin_sio": "Salle de Sport",
"gin_sio_privado": "Salle de Sport Privée",
"grupo": "Groupe",
"grupo_partilhado": "Groupe partagé",
"guardar_altera_es": "Enregistrer les Modifications",
"guardar_cond_mino": "Enregistrer le Résident",
"hist_rico_de_reservas": "Historique des Réservations",
"hor_rio": "Horaire",
"hor_rio_08_00_22_00": "Horaires : 08:00 - 22:00",
"idioma_da_aplica_o": "Langue de l'Application",
"lazer": "Loisirs",
"limpar": "Effacer",
"lista_completa_de_agendamentos_em_todos_os_espa_os_de_lazer": "Liste complète des réservations dans tous les espaces de loisirs",
"localiza_o": "Emplacement",
"manuten_o": "Maintenance",
"manuten_o_e_ocorr_ncias": "Maintenance et Incidents",
"manuten_es_ativas": "Maintenances Actives",
"mapa": "Carte",
"marcar_como_lida": "Marquer comme lu",
"mensagens": "Messages",
"meu_perfil": "Mon Profil",
"min_8_caracteres": "Min. 8 caractères",
"minhas_contas": "Mes Comptes",
"minhas_faturas": "Mes Factures",
"morador": "Résident",
"mudar_permiss_es": "Modifier les Permissions",
"mycondominium": "MyCondominium",
"navega_o_inteligente": "Navigation Intelligente",
"nome_completo": "Nom Complet",
"nome_do_grupo": "Nom du Groupe",
"nome_do_propriet_rio": "Nom du propriétaire",
"nome_do_residente": "Nom du résident",
"notificar": "Notifier",
"notifica_es": "Notifications",
"notifica_es_push_no_navegador": "Notifications Push du Navigateur",
"nova_palavra_passe": "Nouveau Mot de passe",
"nova_reserva": "Nouvelle Réservation",
"novo_registo": "Nouvel Enregistrement",
"n_vel_de_acesso": "Niveau d'Accès",
"pagamentos": "Paiements",
"pagamentos_conclu_dos": "Paiements Terminés",
"pagar": "Payer",
"pago": "Payé",
"palavra_passe": "Mot de passe",
"palavra_passe_atual": "Mot de passe Actuel",
"parque_jogos": "Aire de Jeux",
"parque_de_jogos": "Aire de Jeux",
"permiss_es": "Permissions",
"portal_de_gest_o": "Portail de Gestion",
"prefer_ncias": "Préférences",
"prefer_ncias_da_aplica_o": "Préférences de l'Application",
"prioridade": "Priorité",
"propriet_rio": "Propriétaire",
"pr_ximas_reservas": "Prochaines Réservations",
"quadro_de_avisos": "Tableau d'Affichage",
"quotas_em_atraso": "Cotisations en Retard",
"rio_tejo": "RIO TEJO",
"recarregar_p_gina": "Recharger la Page",
"recibo": "Reçu",
"registar_movimento": "Enregistrer le Mouvement",
"registar_movimento_financeiro": "Enregistrer le Mouvement Financier",
"relat_rios_semanais_autom_ticos": "Rapports Hebdomadaires Automatiques",
"reportar": "Signaler",
"reportar_ocorr_ncia": "Signaler un Incident",
"reportar_problema": "Signaler un Problème",
"reservado_para_cond_mino": "Réservé pour (Résident)",
"reservar_agora": "Réserver Maintenant",
"reservas_m_s": "Réservations (Mois)",
"residencial": "Résidentiel",
"resolver": "Résoudre",
"resolver_problemas": "Résoudre des Problèmes",
"restaurar_base_de_dados": "Restaurer la Base de Données",
"saldo_dispon_vel": "Solde Disponible",
"sal_o_de_festas": "Salle de Fêtes",
"seguran_a": "Sécurité",
"selecionar_moradores": "Sélectionner des Résidents",
"sem_novas_notifica_es": "Aucune nouvelle notification",
"sem_reservas": "Aucune réservation",
"sem_valores_pendentes": "Aucune valeur en attente",
"senha_de_acesso": "Mot de passe d'accès",
"servi_os": "Services",
"sistema": "Système",
"telefone": "Téléphone",
"telem_vel": "Portable",
"terminar_sess_o": "Se Déconnecter",
"tipo": "Type",
"total_pago": "Total Payé",
"total_pendente": "Total en Attente",
"total_agendado": "Total Programmé",
"t_tulo_do_problema": "Titre du Problème",
"via_central": "VIA CENTRAL",
"valor": "Montant (€)",
"valor_pendente": "Montant en Attente (€)",
"vencimento": "Échéance",
"ver_todas_as_reservas": "Voir toutes les Réservations",
"em_progresso": "En Cours",
"receita": "Revenu",
"despesa": "Dépense",
"em_valida_o": "En Validation",
"alta": "Élevée",
"baixa": "Basse",
"novo": "Nouveau",
"resolvido": "Résolu",
"pendente": "En Attente",
"confirmado": "Confirmé",
"m_dia": "Moyenne",
"atrasado": "En Retard",
"ana_silva": "Ana Silva",
"carlos_santos": "Carlos Santos",
"maria_pereira": "Maria Pereira",
"jo_o_ferreira": "João Ferreira",
"sofia_costa": "Sofia Costa",
"administra_o": "Administration",
"utilizador": "Utilisateur",
"pesquisar_cond_mino_por_nome_ou_fra_o": "Rechercher un résident par nom ou unité...",
"pesquisar_transa_o": "Rechercher una transacción...",
"pesquisar_na_lista": "Rechercher dans la liste...",
"pesquisar_fatura": "Rechercher une facture...",
"escreva_a_sua_mensagem": "Écrivez votre message...",
"pesquisar_reserva": "Rechercher une réservation...",
"vs_m_s_passado": "vs. mois dernier",
"algo_correu_mal_erro_na_aplica_o": "Un problème est survenu (Erreur d'Application)",
"email_ou_palavra_passe_incorreta": "E-mail ou Mot de passe incorrect",
"a_entrar": "Connexion en cours...",
"entrar": "Se Connecter",
"nenhuma_ocorr_ncia_encontrada": "Aucun incident trouvé.",
"nenhuma_transa_o_financeira_encontrada": "Aucune transaction financière trouvée.",
"nenhum_cond_mino_encontrado": "Aucun résident trouvé.",
"nenhuma_fatura_encontrada": "Aucune facture trouvée.",
"nenhuma_reserva_encontrada": "Aucune réservation trouvée.",
"tem_a_certeza_que_deseja_terminar_sess_o": "Êtes-vous sûr de vouloir vous déconnecter ?",
"permiss_es_de_utilizador_atualizadas": "Permissions utilisateur mises à jour",
"erro_ao_atualizar_permiss_o": "Erreur lors de la mise à jour de la permission.",
"morador_atualizado_com_sucesso": "Résident mis à jour avec succès",
"morador_adicionado_com_sucesso": "Résident ajouté avec succès",
"erro_ao_guardar_morador": "Erreur lors de l'enregistrement du résident",
"ocorr_ncia_resolvida_com_sucesso": "Incident résolu avec succès",
"ocorr_ncia_atualizada": "Incident mis à jour",
"transa_o_adicionada_com_sucesso": "Transaction ajoutée avec succès",
"erro_ao_guardar_transa_o": "Erreur lors de l'enregistrement de la transaction",
"fatura_emitida_com_sucesso": "Facture émise avec succès",
"erro_ao_emitir_fatura": "Erreur lors de l'émission de la facture",
"fatura_atualizada_para_paga": "Facture mise à jour sur Payée",
"erro_ao_atualizar_fatura": "Erreur lors de la mise à jour de la facture",
"reserva_efetuada_com_sucesso": "Réservation effectuée avec succès",
"reserva_atualizada_com_sucesso": "Réservation mise à jour avec succès",
"erro_ao_fazer_reserva": "Erreur lors de la réservation"
}
};
const LanguageContext = React.createContext();
const LanguageProvider = ({ children }) => {
const [language, setLanguage] = useState(() => {
const stored = localStorage.getItem('condo_language');
if (stored) return stored;
const cookieMatch = document.cookie.match(/googtrans=\/pt\/([a-z]{2})/);
if (cookieMatch) return cookieMatch[1];
return 'pt';
});
const t = (key) => {
return translations[language]?.[key] || translations['pt']?.[key] || key;
};
const changeLanguage = (lang) => {
setLanguage(lang);
localStorage.setItem('condo_language', lang);
document.cookie = `googtrans=/pt/${lang}; path=/;`;
const domain = window.location.hostname;
if(domain !== 'localhost' && domain !== '127.0.0.1' && domain !== '') {
document.cookie = `googtrans=/pt/${lang}; domain=${domain}; path=/;`;
document.cookie = `googtrans=/pt/${lang}; domain=.${domain}; path=/;`;
}
};
return (
<LanguageContext.Provider value={{ language, changeLanguage, t }}>
{children}
</LanguageContext.Provider>
);
};
const useTranslation = () => React.useContext(LanguageContext);
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
this.setState({ error, errorInfo });
console.error("ErrorBoundary caught an error:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: '20px', backgroundColor: '#fee2e2', color: '#991b1b', fontFamily: 'sans-serif', height: '100vh' }}>
<h1 style={{ fontSize: '24px', fontWeight: 'bold' }}>Algo correu mal (Erro na Aplicação)</h1>
<pre style={{ marginTop: '20px', whiteSpace: 'pre-wrap', backgroundColor: '#fef2f2', padding: '15px', border: '1px solid #f87171' }}>
{this.state.error && this.state.error.toString()}
<br />
{this.state.errorInfo && this.state.errorInfo.componentStack}
</pre>
<button onClick={() => window.location.reload()} style={{ marginTop: '20px', padding: '10px 20px', backgroundColor: '#dc2626', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}>{defaultTranslate('recarregar_p_gina')}</button>
</div>
);
}
return this.props.children;
}
}
const auth = getAuth(app);
const DB_STATUS = {
PAGO: 'Pago',
PENDENTE: 'Pendente',
ATRASADO: 'Atrasado',
RESOLVIDO: 'Resolvido',
EM_PROGRESSO: 'Em Progresso',
EM_VALIDACAO: 'Em Validação',
NOVO: 'Novo',
CONFIRMADO: 'Confirmado',
};
const STATUS_ALIASES = {
'paid': DB_STATUS.PAGO, 'pagado': DB_STATUS.PAGO, 'payé': DB_STATUS.PAGO, 'paye': DB_STATUS.PAGO, 'pago': DB_STATUS.PAGO,
'pending': DB_STATUS.PENDENTE, 'pendiente': DB_STATUS.PENDENTE, 'en attente': DB_STATUS.PENDENTE, 'pendente': DB_STATUS.PENDENTE,
'resolved': DB_STATUS.RESOLVIDO, 'resuelto': DB_STATUS.RESOLVIDO, 'résolu': DB_STATUS.RESOLVIDO, 'resolvido': DB_STATUS.RESOLVIDO,
'in progress': DB_STATUS.EM_PROGRESSO, 'en progreso': DB_STATUS.EM_PROGRESSO, 'em progresso': DB_STATUS.EM_PROGRESSO,
'overdue': DB_STATUS.ATRASADO, 'atrasado': DB_STATUS.ATRASADO,
'confirmed': DB_STATUS.CONFIRMADO, 'confirmado': DB_STATUS.CONFIRMADO,
'new': DB_STATUS.NOVO, 'nuevo': DB_STATUS.NOVO, 'nouveau': DB_STATUS.NOVO, 'novo': DB_STATUS.NOVO,
};
const normalizeStatus = (status) => {
if (!status) return status;
const lower = String(status).trim().toLowerCase();
return STATUS_ALIASES[lower] || status;
};
const isPaidStatus = (status) => normalizeStatus(status) === DB_STATUS.PAGO;
const isPendingStatus = (status) => normalizeStatus(status) === DB_STATUS.PENDENTE;
const isResolvedStatus = (status) => normalizeStatus(status) === DB_STATUS.RESOLVIDO;
const normalizeRecord = (path, id, val) => {
if (!val || typeof val !== 'object') return { id, ...(val || {}) };
switch (path) {
case 'condominos':
return {
id,
unit: val.unit || val.fracao || '',
name: val.name || val.proprietario || '',
contact: val.contact || val.contacto || '',
email: val.email || '',
password: val.password,
photoUrl: val.photoUrl,
status: normalizeStatus(val.status || val.estado) || DB_STATUS.PAGO,
pending: Number(val.pending ?? val.divida ?? 0),
role: val.role || 'morador',
};
case 'faturas': {
const normalizedStatus = val.status === DB_STATUS.EM_VALIDACAO ? DB_STATUS.PAGO : normalizeStatus(val.status);
return { id, ...val, status: normalizedStatus || val.status };
}
case 'financas':
return {
id,
type: val.type || (val.tipo === 'receita' ? 'income' : val.tipo === 'despesa' ? 'expense' : val.type) || 'expense',
category: val.category || val.categoria || '',
date: val.date || val.data || '',
amount: Number(val.amount ?? val.valor ?? 0),
desc: val.desc || val.descricao || '',
};
case 'manutencao':
return {
id,
title: val.title || val.titulo || '',
location: val.location || val.local || '',
priority: val.priority || val.prioridade || 'Média',
status: normalizeStatus(val.status) || val.status || DB_STATUS.NOVO,
date: val.date || val.data || '',
moradorId: val.moradorId || val.morador_id || '',
};
default:
return { id, ...val };
}
};
const parseRealtimeSnapshot = (path, data, userRole, currentUserId, sortFunc = null) => {
let parsed = Object.entries(data).map(([id, val]) => normalizeRecord(path, id, val));
if (userRole !== 'admin' && (path === 'manutencao' || path === 'reservas')) {
parsed = parsed.filter(item => item.moradorId === currentUserId);
}
if (sortFunc) parsed = [...parsed].sort(sortFunc);
return parsed;
};
const sortByDateDesc = (a, b) => new Date(b.date) - new Date(a.date);
const sortByVencimentoDesc = (a, b) => new Date(b.dataVencimento) - new Date(a.dataVencimento);
const defaultTranslate = (key) => translations['pt']?.[key] || key;
const INITIAL_RESIDENTS = [
{ id: '1', unit: '1º Esq', name: 'Ana Silva', contact: '912 345 678', email: 'ana.silva@email.com', status: DB_STATUS.PAGO, pending: 0, role: 'morador' },
{ id: '2', unit: '1º Dto', name: 'Carlos Santos', contact: '965 432 109', email: 'carlos.s@email.com', status: DB_STATUS.PENDENTE, pending: 45.00, role: 'morador' },
{ id: '3', unit: '2º Esq', name: 'Maria Pereira', contact: '933 221 110', email: 'maria.p@email.com', status: DB_STATUS.PAGO, pending: 0, role: 'morador' },
{ id: '4', unit: '2º Dto', name: 'João Ferreira', contact: '918 765 432', email: 'joao.f@email.com', status: DB_STATUS.ATRASADO, pending: 135.00, role: 'morador' },
{ id: '5', unit: '3º Esq', name: 'Sofia Costa', contact: '922 334 455', email: 'sofia.c@email.com', status: DB_STATUS.PAGO, pending: 0, role: 'morador' },
];
const INITIAL_FINANCES = [
{ id: '1', type: 'income', category: 'Quotas Mensais', date: '2023-10-01', amount: 2250.00, desc: 'Pagamento de quotas Outubro' },
{ id: '2', type: 'expense', category: 'Limpeza', date: '2023-10-02', amount: 450.00, desc: 'Serviço de Limpeza Semanal' },
{ id: '3', type: 'expense', category: 'Elevadores', date: '2023-10-05', amount: 120.00, desc: 'Manutenção Mensal' },
{ id: '4', type: 'income', category: 'Aluguer Salão', date: '2023-10-10', amount: 50.00, desc: 'Reserva 2º Dto' },
{ id: '5', type: 'expense', category: 'Jardinagem', date: '2023-10-12', amount: 85.00, desc: 'Poda de árvores' },
];
const INITIAL_ISSUES = [
{ id: '1', title: 'Lâmpada fundida no Hall', location: 'R/C', status: DB_STATUS.NOVO, priority: 'Baixa', date: '2023-10-15' },
{ id: '2', title: 'Porta da garagem não fecha', location: 'Garagem -1', status: DB_STATUS.EM_PROGRESSO, priority: 'Alta', date: '2023-10-14' },
{ id: '3', title: 'Infiltração no teto', location: '3º Dto', status: DB_STATUS.RESOLVIDO, priority: 'Média', date: '2023-10-10' },
];
const INITIAL_BOOKINGS = [
{ id: '1', facility: 'hall', facilityName: 'Salão de Festas', date: '2023-10-25', time: '14:00 - 20:00', resident: 'Ana Silva', status: DB_STATUS.CONFIRMADO, cost: 50 },
{ id: '2', facility: 'gym', facilityName: 'Ginásio', date: '2023-10-20', time: '09:00 - 10:00', resident: 'Carlos Santos', status: DB_STATUS.CONFIRMADO, cost: 0 },
{ id: '3', facility: 'park', facilityName: 'Parque de Jogos', date: '2023-10-22', time: '18:00 - 19:00', resident: 'Sofia Costa', status: DB_STATUS.PENDENTE, cost: 10 },
];
const INITIAL_NOTIFICATIONS = [
{ id: '1', message: 'Nova reserva: Salão de Festas (25 Out)', time: 'Há 1 hora', type: 'info', read: false },
{ id: '2', message: 'Nova quota paga: Maria Pereira', time: 'Há 2 horas', type: 'success', read: false },
{ id: '3', message: 'Manutenção urgente reportada', time: 'Há 5 horas', type: 'warning', read: false },
];
// --- VALIDAÇÕES OFICIAIS ---
function validarNIF(nif) {
nif = String(nif).replace(/\s+/g, '');
return /^\d{9}$/.test(nif);
}
function validarDocumento(doc) {
let docStr = doc.replace(/[\s-]/g, '').toUpperCase();
// Muitos utilizadores inserem apenas os 8 dígitos do NIC, o que não tem check-digit na própria string
if (/^\d{8}$/.test(docStr)) {
return true;
}
if (/^\d{9}$/.test(docStr)) {
let checkDigitValue = parseInt(docStr.charAt(docStr.length - 1), 10);
let soma = 0;
for (let i = 0; i < docStr.length - 1; i++) {
soma += parseInt(docStr.charAt(i), 10) * (docStr.length - i);
}
let resto = soma % 11;
let expectedDigit = (resto === 0 || resto === 1) ? 0 : (11 - resto);
return expectedDigit === checkDigitValue;
}
if (docStr.length === 12) {
let sum = 0;
let isSecond = false;
for (let i = docStr.length - 1; i >= 0; i--) {
let charCode = docStr.charCodeAt(i);
let val = 0;
if (charCode >= 48 && charCode <= 57) val = charCode - 48;
else if (charCode >= 65 && charCode <= 90) val = charCode - 55;
else return false;
if (isSecond) {
val *= 2;
if (val >= 36) val -= 36;
}
sum += val;
isSecond = !isSecond;
}
return (sum % 36) === 0;
}
return false;
}
const Modal = ({ isOpen, onClose, title, children }) => {
const { t, language, changeLanguage } = useTranslation();
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in">
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-2xl w-full max-w-lg overflow-hidden animate-slide-up transition-colors">
<div className="flex justify-between items-center p-6 border-b border-slate-100 dark:border-dark-border bg-slate-50 dark:bg-dark-bg">
<h3 className="font-bold text-lg text-slate-800 dark:text-white">{title}</h3>
<button onClick={onClose} className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
<X size={24} />
</button>
</div>
<div className="p-6 max-h-[80vh] overflow-y-auto">{children}</div>
</div>
</div>
);
};
const InputGroup = ({ label, name, type = 'text', value, onChange, placeholder, required = false, options = null, disabled = false }) => {
const { t } = useTranslation();
return (
<div className="mb-4">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
{label} {required && <span className="text-red-500">*</span>}
</label>
{options ? (
<select
name={name}
value={value}
onChange={onChange}
disabled={disabled}
className="w-full px-3 py-2 border border-slate-300 dark:border-dark-border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-dark-card dark:text-white disabled:bg-slate-100 dark:disabled:bg-slate-800 disabled:text-slate-500 dark:disabled:text-slate-400 transition-colors"
>
{options.map(opt => (
<option key={opt.value} value={opt.value} className="dark:bg-dark-bg">{opt.label}</option>
))}
</select>
) : (
<input
type={type}
name={name}
value={value}
onChange={onChange}
placeholder={placeholder}
required={required}
disabled={disabled}
className="w-full px-3 py-2 border border-slate-300 dark:border-dark-border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-dark-card dark:text-white dark:placeholder-slate-500 disabled:bg-slate-100 dark:disabled:bg-slate-800 disabled:text-slate-500 dark:disabled:text-slate-400 transition-colors"
/>
)}
</div>
);
};
const SidebarItem = ({ icon: Icon, label, active, onClick }) => {
const { t } = useTranslation();
return (
<button
onClick={onClick}
className={`flex items-center w-full gap-3 px-4 py-3 text-sm font-medium transition-all rounded-lg ${active
? 'bg-blue-600 text-white shadow-md translate-x-1'
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-dark-card hover:text-blue-600 dark:hover:text-blue-400'
}`}
>
<Icon size={20} />
<span>{label}</span>
</button>
);
};
const Card = ({ title, value, icon: Icon, trend, trendValue, color, subtitle }) => {
const { t } = useTranslation();
return (
<div className="bg-white dark:bg-dark-surface p-6 rounded-xl shadow-sm border border-slate-100 dark:border-dark-border hover:shadow-md transition-all">
<div className="flex justify-between items-start">
<div>
<p className="text-sm font-medium text-slate-500 dark:text-dark-mute">{title}</p>
<h3 className="text-2xl font-bold text-slate-800 dark:text-white mt-2">{value}</h3>
</div>
<div className={`p-3 rounded-lg ${color}`}>
<Icon size={24} className="text-white" />
</div>
</div>
<div className="mt-4 flex items-center text-sm justify-between">
<div className="flex items-center">
{trend === 'up' ? (
<span className="text-green-500 flex items-center font-medium bg-green-50 dark:bg-green-900/20 px-2 py-0.5 rounded-full">
<TrendingUp size={14} className="mr-1" /> {trendValue}
</span>
) : trend === 'down' ? (
<span className="text-red-500 flex items-center font-medium bg-red-50 dark:bg-red-900/20 px-2 py-0.5 rounded-full">
<TrendingDown size={14} className="mr-1" /> {trendValue}
</span>
) : (
<span className="text-slate-400 dark:text-slate-500 font-medium bg-slate-50 dark:bg-dark-card px-2 py-0.5 rounded-full">
</span>
)}
</div>
<span className="text-slate-400 dark:text-dark-mute text-xs">{subtitle || t('vs_m_s_passado')}</span>
</div>
</div>
);
};
const Badge = ({ status }) => {
const { t } = useTranslation();
const styles = {
[DB_STATUS.PAGO]: 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800',
'Em dia': 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800',
[DB_STATUS.RESOLVIDO]: 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800',
[DB_STATUS.PENDENTE]: 'bg-yellow-100 text-yellow-700 border-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400 dark:border-yellow-800',
[DB_STATUS.EM_VALIDACAO]: 'bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-900/30 dark:text-orange-400 dark:border-orange-800',
[DB_STATUS.EM_PROGRESSO]: 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800',
[DB_STATUS.ATRASADO]: 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800',
[DB_STATUS.CONFIRMADO]: 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800',
[DB_STATUS.NOVO]: 'bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-900/30 dark:text-purple-400 dark:border-purple-800',
'Receita': 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800',
'Despesa': 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800',
'Alta': 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800',
'Média': 'bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-900/30 dark:text-orange-400 dark:border-orange-800',
'Baixa': 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800',
};
const translateStatus = (s) => {
if (!s) return s;
const key = s.toLowerCase().replace(/[^a-z0-9]+/g, '_');
return t(key);
};
const canonicalStatus = normalizeStatus(status);
return (
<span className={`px-2.5 py-1 rounded-full text-xs font-semibold border ${styles[canonicalStatus] || styles[status] || 'bg-gray-100 text-gray-700 border-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700'}`}>
{translateStatus(canonicalStatus)}
</span>
);
};
const LoginView = ({ onLogin }) => {
const { t, language, changeLanguage } = useTranslation();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
if (isLoading) return;
setError('');
setIsLoading(true);
const success = await onLogin(email, password);
setIsLoading(false);
if (!success) {
setError(t('email_ou_palavra_passe_incorreta'));
setTimeout(() => {
setError('');
}, 5000);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-slate-100 dark:bg-dark-bg p-4 transition-colors duration-300 font-sans">
<div className="bg-white dark:bg-dark-surface p-8 rounded-xl shadow-2xl w-full max-w-md animate-fade-in border border-slate-100 dark:border-dark-border">
<div className="text-center mb-8">
<div className="inline-flex p-3 bg-blue-100 dark:bg-blue-900/30 rounded-full mb-4 text-blue-600 dark:text-blue-400">
<Building2 size={32} />
</div>
<h1 className="text-2xl font-bold text-slate-800 dark:text-white">{t('mycondominium')}</h1>
<p className="text-slate-500 dark:text-gray-400 mt-2">{t('portal_de_gest_o')}</p>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">{t('email')}</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-4 py-3 rounded-lg border border-slate-300 dark:border-dark-border focus:ring-2 focus:ring-blue-500 bg-white dark:bg-dark-card text-slate-900 dark:text-white transition-colors"
placeholder={t('endere_o_de_email')}
autoFocus
required
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">{t('palavra_passe')}</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-3 rounded-lg border border-slate-300 dark:border-dark-border focus:ring-2 focus:ring-blue-500 bg-white dark:bg-dark-card text-slate-900 dark:text-white transition-colors"
placeholder={t('senha_de_acesso')}
required
/>
</div>
{error && (
<div className="p-3 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-sm rounded-lg flex items-center gap-2 animate-fade-in">
<AlertCircle size={16} />
{error}
</div>
)}
<div className="space-y-4">
<button type="submit" disabled={isLoading} className={`w-full ${isLoading ? 'bg-blue-400 dark:bg-blue-600/50 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-700 shadow-lg shadow-blue-500/30'} text-white font-bold py-3 rounded-lg transition-colors flex justify-center items-center gap-2`}>
{isLoading ? (
<>
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
A entrar...
</>
) : (
t('entrar')
)}
</button>
</div>
</form>
</div>
</div>
);
};
function App() {
const { t, language, changeLanguage } = useTranslation();
const [activeTab, setActiveTab] = useState('dashboard');
const [isSidebarOpen, setSidebarOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [theme, setTheme] = useState('system');
const [isAuthenticated, setIsAuthenticated] = useState(() => {
return sessionStorage.getItem('condo_auth') === 'true';
});
const [userRole, setUserRole] = useState(() => {
return sessionStorage.getItem('condo_role') || 'morador';
});
const [currentUserName, setCurrentUserName] = useState(() => {
return sessionStorage.getItem('condo_user_name') || t('utilizador');
});
const [currentUserId, setCurrentUserId] = useState(() => {
return sessionStorage.getItem('condo_user_id') || '0';
});
const [userStatus, setUserStatus] = useState(() => {
return sessionStorage.getItem('condo_user_status') || 'aprovado';
});
const handleLogin = async (email, password) => {
try {
const userCredential = await signInWithEmailAndPassword(auth, email, password);
let role = 'morador';
let userName = t('utilizador');
let userId = userCredential.user.uid;
let status = 'aprovado';
if (email.toLowerCase().includes('admin')) {
role = 'admin';
userName = t('administra_o');
} else {
const residentUser = residents.find(r => r.id === userId || (r.email && r.email.toLowerCase() === email.toLowerCase()));
if (residentUser) {
role = residentUser.role || 'morador';
userName = residentUser.name + (residentUser.unit && residentUser.unit !== t('pendente') ? ` (${residentUser.unit})` : '');
userId = residentUser.id || userId;
status = residentUser.status || 'aprovado';
} else {
status = 'pendente'; // Fallback if missing
}
}
sessionStorage.setItem('condo_auth', 'true');
sessionStorage.setItem('condo_role', role);
sessionStorage.setItem('condo_user_name', userName);
sessionStorage.setItem('condo_user_id', userId);
sessionStorage.setItem('condo_user_status', status);
setIsAuthenticated(true);
setUserRole(role);
setCurrentUserName(userName);
setCurrentUserId(userId);
setUserStatus(status);
return true;
} catch (error) {
console.log("Firebase Auth falhou, a tentar conta local...", error);
let role = null;
let userName = t('utilizador');
let userId = 'local_' + Date.now();
let status = 'aprovado';
if (email === 'administradores@gmail.com' && password === 'admin123') {
role = 'admin';
userName = t('administra_o');
userId = 'admin_001';
} else {
const residentUser = residents.find(r => r.email && r.email.toLowerCase() === email.toLowerCase());
if (residentUser && (password === residentUser.password || (!residentUser.password && password === residentUser.contact) || password === '1234')) {
role = residentUser.role || 'morador';
userName = residentUser.name + (residentUser.unit && residentUser.unit !== t('pendente') ? ` (${residentUser.unit})` : '');
userId = residentUser.id || userId;
status = residentUser.status || 'aprovado';
}
}
if (role) {
sessionStorage.setItem('condo_auth', 'true');
sessionStorage.setItem('condo_role', role);
sessionStorage.setItem('condo_user_name', userName);
sessionStorage.setItem('condo_user_id', userId);
sessionStorage.setItem('condo_user_status', status);
setIsAuthenticated(true);
setUserRole(role);
setCurrentUserName(userName);
setCurrentUserId(userId);
setUserStatus(status);
return true;
}
return false;
}
};
const handleLogout = () => {
openConfirm(t('tem_a_certeza_que_deseja_terminar_sess_o'), () => {
sessionStorage.removeItem('condo_auth');
sessionStorage.removeItem('condo_role');
sessionStorage.removeItem('condo_user_name');
sessionStorage.removeItem('condo_user_id');
sessionStorage.removeItem('condo_user_status');
setIsAuthenticated(false);
setUserRole(null);
setCurrentUserName(t('utilizador'));
setCurrentUserId('0');
setUserStatus('aprovado');
setActiveTab('dashboard');
});
};
const [residents, setResidents] = useState([]);
const [finances, setFinances] = useState([]);
const [issues, setIssues] = useState([]);
const [bookings, setBookings] = useState([]);
const [invoices, setInvoices] = useState([]);
const [faturas, setFaturas] = useState([]);
const [messages, setMessages] = useState([]);
const [newMessageText, setNewMessageText] = useState('');
const [activeChat, setActiveChat] = useState({ type: 'global', id: 'global', name: t('f_rum_do_condom_nio') });
const [chatGroups, setChatGroups] = useState([]);
const [adminProfile, setAdminProfile] = useState({});
const [isCreateGroupModalOpen, setIsCreateGroupModalOpen] = useState(false);
const [newGroupName, setNewGroupName] = useState('');
const [newGroupMembers, setNewGroupMembers] = useState([]);
const userRoleRef = useRef(userRole);
const currentUserIdRef = useRef(currentUserId);
userRoleRef.current = userRole;
currentUserIdRef.current = currentUserId;
useEffect(() => {
const loadData = (path, setter, sortFunc = null) => {
return onValue(ref(db, path), (snapshot) => {
const data = snapshot.val();
if (data) {
setter(parseRealtimeSnapshot(
path,
data,
userRoleRef.current,
currentUserIdRef.current,
sortFunc
));
} else {
setter([]);
}
}, (error) => console.error(`Erro ao carregar ${path}:`, error));
};
const unsubResidents = loadData('condominos', setResidents);
const unsubFinances = loadData('financas', setFinances, sortByDateDesc);
// Manutenção: carregar inicialmente e depois ligar listeners por child para atualizações em tempo real
const issuesRef = ref(db, 'manutencao');
// initial load
get(issuesRef).then(snapshot => {
const data = snapshot.val();
if (data) {
setIssues(parseRealtimeSnapshot('manutencao', data, userRoleRef.current, currentUserIdRef.current, sortByDateDesc));
} else {
setIssues([]);
}
}).catch(err => console.error('Erro ao carregar manutencao inicial:', err));
const handleChildAdded = (snap) => {
const rec = normalizeRecord('manutencao', snap.key, snap.val());
if (userRoleRef.current !== 'admin' && String(rec.moradorId) !== String(currentUserIdRef.current)) return;
setIssues(prev => {
const exists = prev.some(p => p.id === rec.id);
if (exists) return prev.map(p => p.id === rec.id ? rec : p).sort(sortByDateDesc);
return [rec, ...prev].sort(sortByDateDesc);
});
};
const handleChildChanged = (snap) => {
const rec = normalizeRecord('manutencao', snap.key, snap.val());
setIssues(prev => prev.map(p => p.id === rec.id ? rec : p).sort(sortByDateDesc));
};
const handleChildRemoved = (snap) => {
setIssues(prev => prev.filter(p => p.id !== snap.key));
};
const unsubIssuesAdded = onChildAdded(issuesRef, handleChildAdded, (err) => console.error('Erro onChildAdded manutencao:', err));
const unsubIssuesChanged = onChildChanged(issuesRef, handleChildChanged, (err) => console.error('Erro onChildChanged manutencao:', err));
const unsubIssuesRemoved = onChildRemoved(issuesRef, handleChildRemoved, (err) => console.error('Erro onChildRemoved manutencao:', err));
const unsubBookings = loadData('reservas', setBookings, sortByDateDesc);
const unsubInvoices = loadData('faturacao', setInvoices, sortByDateDesc);
const unsubFaturas = loadData('faturas', setFaturas, sortByVencimentoDesc);
const unsubGroups = loadData('grupos_chat', setChatGroups);
const unsubAdmin = onValue(ref(db, 'configuracoes/admin_profile'), (snapshot) => {
if (snapshot.exists()) setAdminProfile(snapshot.val());
}, (error) => console.error('Erro ao carregar perfil admin:', error));
return () => {
unsubResidents();
unsubFinances();
// unsubscribe manutencao child listeners
try { unsubIssuesAdded(); } catch(e){/* ignore */}
try { unsubIssuesChanged(); } catch(e){/* ignore */}
try { unsubIssuesRemoved(); } catch(e){/* ignore */}
unsubBookings();
unsubInvoices();
unsubFaturas();
unsubGroups();
unsubAdmin();
};
}, []);
useEffect(() => {
if (!isAuthenticated) return;
const refreshFilteredCollections = async () => {
const filteredPaths = [
{ path: 'manutencao', setter: setIssues, sort: sortByDateDesc },
{ path: 'reservas', setter: setBookings, sort: sortByDateDesc },
];
for (const { path, setter, sort } of filteredPaths) {
try {
const snapshot = await get(ref(db, path));
const data = snapshot.val();
setter(data ? parseRealtimeSnapshot(path, data, userRole, currentUserId, sort) : []);
} catch (error) {
console.error(`Erro ao atualizar ${path}:`, error);
}
}
};
refreshFilteredCollections();
}, [isAuthenticated, userRole, currentUserId]);
useEffect(() => {
if (!isAuthenticated || !currentUserId) {
setNotificationsList([]);
return;
}
const targetFolder = userRole === 'admin' ? 'admin' : currentUserId;
const path = `notificacoes/${targetFolder}`;
const unsub = onValue(ref(db, path), (snapshot) => {
const data = snapshot.val();
if (data) {
let parsed = Object.entries(data).map(([id, val]) => ({ id, ...val }));
parsed = parsed.sort((a,b) => b.timestamp - a.timestamp);
setNotificationsList(parsed);
} else {
setNotificationsList([]);
}
}, (error) => console.error(`Erro ao carregar notificações:`, error));
return () => unsub();
}, [isAuthenticated, currentUserId, userRole]);
useEffect(() => {
let path = 'mural_mensagens';
if (activeChat.type === 'private') {
path = `mensagens_privadas/${[currentUserId, activeChat.id].sort().join('_')}`;
} else if (activeChat.type === 'group') {
path = `mensagens_grupo/${activeChat.id}`;
}
const unsub = onValue(ref(db, path), (snapshot) => {
const data = snapshot.val();
if (data) {
let parsed = Object.entries(data).map(([id, val]) => ({ id, ...val }));
parsed = parsed.sort((a,b) => a.timestamp - b.timestamp);
setMessages(parsed);
} else {
setMessages([]);
}
}, (error) => console.error(`Erro ao carregar mensagens de ${path}:`, error));
return () => unsub();
}, [activeChat, currentUserId]);
useEffect(() => {
if (userRole === 'admin' && residents.length > 0 && faturas.length > 0) {
let hasUpdates = false;
const updates = {};
residents.forEach((resident) => {
const residentFaturas = faturas.filter(f => f.moradorId === resident.id && !isPaidStatus(f.status));
const actualPending = residentFaturas.reduce((acc, f) => acc + Number(f.valor), 0);
const actualStatus = actualPending > 0 ? DB_STATUS.PENDENTE : DB_STATUS.PAGO;
if (Number(resident.pending) !== actualPending || normalizeStatus(resident.status) !== actualStatus) {
updates[`condominos/${resident.id}/pending`] = actualPending;
updates[`condominos/${resident.id}/status`] = actualStatus;
hasUpdates = true;
}
});
if (hasUpdates) {
update(ref(db), updates).catch(err => console.error("Erro na sincronização:", err));
}
}
}, [faturas, residents, userRole]);
const [notificationsList, setNotificationsList] = useState([]);
const [isNotificationsOpen, setNotificationsOpen] = useState(false);
const [activeModal, setActiveModal] = useState(null);
const [editingItem, setEditingItem] = useState(null);
const [confirmDialog, setConfirmDialog] = useState({ isOpen: false, message: '', onConfirm: null });
const openConfirm = (message, onConfirm) => setConfirmDialog({ isOpen: true, message, onConfirm });
const [notification, setNotification] = useState(null);
const notificationRef = useRef(null);
const initialResidentForm = { unit: '', name: '', contact: '', email: '', status: DB_STATUS.PAGO, pending: 0, role: 'morador' };
const initialFinanceForm = { type: 'expense', category: '', amount: '', desc: '', date: new Date().toISOString().split('T')[0] };
const initialIssueForm = { title: '', location: '', priority: 'Média', status: 'Novo', date: new Date().toISOString().split('T')[0] };
const initialBookingForm = { facility: 'gym', date: new Date().toISOString().split('T')[0], time: '', resident: '', cost: 0 };
const initialFaturaForm = { moradorId: '', categoria: '', valor: '', dataVencimento: new Date().toISOString().split('T')[0] };
const [formData, setFormData] = useState({});
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove('dark');
if (theme === 'dark') {
root.classList.add('dark');
} else if (theme === 'system') {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
root.classList.add('dark');
}
}
}, [theme]);
useEffect(() => {
if (notification) {
const timer = setTimeout(() => setNotification(null), 3000);
return () => clearTimeout(timer);
}
}, [notification]);
useEffect(() => {
function handleClickOutside(event) {
if (notificationRef.current && !notificationRef.current.contains(event.target)) {
setNotificationsOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [notificationRef]);
const toggleSidebar = () => setSidebarOpen(!isSidebarOpen);
const totalIncome = finances.filter(f => f.type === 'income').reduce((acc, curr) => acc + Number(curr.amount), 0);
const totalExpense = finances.filter(f => f.type === 'expense').reduce((acc, curr) => acc + Number(curr.amount), 0);
const balance = totalIncome - totalExpense;
const activeIssuesCount = issues.filter(i => !isResolvedStatus(i.status)).length;
const unreadNotifications = notificationsList.filter(n => !n.read).length;
const filteredResidents = residents.filter(r =>
(r.name || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
(r.unit || '').toLowerCase().includes(searchQuery.toLowerCase())
);
const sendSystemNotification = async (message, type = 'info', targetUserId = 'admin') => {
const newNotif = { timestamp: Date.now(), message, time: 'Agora', type, read: false };
const writeNotification = async (folder) => {
const newRef = push(ref(db, `notificacoes/${folder}`));
await set(newRef, newNotif);
const targetFolder = userRole === 'admin' ? 'admin' : currentUserId;
if (folder === targetFolder) {
setNotificationsList(prev => [{ id: newRef.key, ...newNotif }, ...prev].sort((a, b) => b.timestamp - a.timestamp));
}
};
if (targetUserId === 'todos') {
await Promise.all([
...residents.map(r => writeNotification(r.id)),
writeNotification('admin'),
]);
} else {
await writeNotification(targetUserId);
}
};
const handleMarkAsRead = async (notifId) => {
const targetFolder = userRole === 'admin' ? 'admin' : currentUserId;
const notifRef = ref(db, `notificacoes/${targetFolder}/${notifId}`);
await update(notifRef, { read: true });
};
const showNotification = (message, type = 'success') => {
setNotification({ message, type });
};
const handleClearNotifications = async () => {
const targetFolder = userRole === 'admin' ? 'admin' : currentUserId;
await set(ref(db, `notificacoes/${targetFolder}`), null);
setNotificationsOpen(false);
};
const handleOpenModal = (type, item = null, defaultFacility = null) => {
setEditingItem(item);
setActiveModal(type);
if (type === 'resident') {
setFormData(item || initialResidentForm);
} else if (type === 'finance') {
setFormData(initialFinanceForm);
} else if (type === 'issue') {
setFormData(initialIssueForm);
} else if (type === 'emitir_fatura') {
setFormData(initialFaturaForm);
} else if (type === 'booking') {
const baseForm = { ...initialBookingForm };
if (defaultFacility) baseForm.facility = defaultFacility;
// Preenche sempre o nome do utilizador logado por defeito
baseForm.resident = currentUserName;
setFormData(baseForm);
}
};
const handleCloseModal = () => {
setActiveModal(null);
setEditingItem(null);
setFormData({});
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => {
const newData = { ...prev, [name]: value };
if (name === 'facility' || name === 'time') {
let cost = 0;
if (newData.facility === 'hall') cost = 50;
if (newData.facility === 'park') cost = 10;
newData.cost = cost;
}
return newData;
});
};
const handleToggleRole = async (id) => {
try {
const resident = residents.find(r => r.id === id);
if (resident) {
const newRole = resident.role === 'admin' ? 'morador' : 'admin';
const residentRef = ref(db, `condominos/${id}`);
await set(residentRef, { ...resident, role: newRole });
setResidents(prev => prev.map(r => r.id === id ? { ...r, role: newRole } : r));
showNotification(t('permiss_es_de_utilizador_atualizadas'), 'success');
}
} catch (error) {
console.error("Erro ao atualizar permissão:", error);
showNotification(t('erro_ao_atualizar_permiss_o'), "error");
}
};
const handleSaveResident = async (e) => {
e.preventDefault();
try {
if (editingItem) {
const residentRef = ref(db, `condominos/${editingItem.id}`);
const updatedData = {
...editingItem,
unit: formData.unit || '',
name: formData.name || '',
contact: formData.contact || '',
email: formData.email || '',
status: normalizeStatus(formData.status) || DB_STATUS.PAGO,
pending: Number(formData.pending) || 0,
role: formData.role || 'morador'
};
if (formData.password) {
updatedData.password = formData.password;
}
await set(residentRef, updatedData);
setResidents(prev => prev.map(r => r.id === editingItem.id ? { ...updatedData } : r));
showNotification(`Condómino ${formData.name} atualizado`);
} else {
const residentsListRef = ref(db, 'condominos');
const newResidentRef = push(residentsListRef);
const newResident = {
unit: formData.unit || '',
name: formData.name || '',
contact: formData.contact || '',
email: formData.email || '',
password: formData.password || '1234',
status: normalizeStatus(formData.status) || DB_STATUS.PAGO,
pending: Number(formData.pending) || 0,
role: formData.role || 'morador'
};
await set(newResidentRef, newResident);
setResidents(prev => [...prev, { id: newResidentRef.key, ...newResident }]);
showNotification(`Novo condómino ${formData.name} adicionado`);
}
handleCloseModal();
} catch (error) {
console.error("Erro ao guardar no Firebase:", error);
showNotification("Erro ao guardar os dados.", "error");
}
};
const handleDeleteResident = async (id) => {
openConfirm('Tem a certeza que deseja eliminar este condómino?', async () => {
try {
const residentRef = ref(db, `condominos/${id}`);
await remove(residentRef);
setResidents(prev => prev.filter(r => r.id !== id));
showNotification('Condómino removido', 'error');
} catch (error) {
console.error("Erro ao eliminar no Firebase:", error);
showNotification("Erro ao eliminar.", "error");
}
});
};
const handleSaveFinance = async (e) => {
e.preventDefault();
if (!formData.amount || !formData.category || !formData.date) {
showNotification("Preencha todos os campos obrigatórios.", "error");
return;
}
try {
const amount = Number(formData.amount);
const newFinanceRef = push(ref(db, 'financas'));
const newFinance = { ...formData, amount };
await set(newFinanceRef, newFinance);
setFinances(prev => [{ id: newFinanceRef.key, ...newFinance }, ...prev].sort(sortByDateDesc));
if (formData.type === 'expense') {
sendSystemNotification(`Nova despesa registada: ${formData.category} - ${amount.toFixed(2)}`, 'warning', 'admin');
} else {
sendSystemNotification(`Nova receita registada: ${formData.category} - ${amount.toFixed(2)}`, 'success', 'admin');
}
showNotification(`Movimento de ${amount}€ registado`);
handleCloseModal();
} catch (error) {
console.error("Erro ao guardar finanças:", error);
showNotification("Erro ao guardar movimento.", "error");
}
};
const handleSaveIssue = async (e) => {
e.preventDefault();
if (!formData.title || !formData.location) {
showNotification("Preencha todos os campos obrigatórios.", "error");
return;
}
try {
const newIssueRef = push(ref(db, 'manutencao'));
const newIssue = { ...formData, moradorId: currentUserId };
await set(newIssueRef, newIssue);
sendSystemNotification(`Nova ocorrência reportada: ${formData.title} (${formData.location})`, 'warning', 'admin');
if (userRole !== 'admin') {
sendSystemNotification(`A sua ocorrência "${formData.title}" foi reportada com sucesso.`, 'info', currentUserId);
}
showNotification('Nova ocorrência reportada', 'warning');
handleCloseModal();
} catch (error) {
console.error("Erro ao reportar ocorrência:", error);
showNotification("Erro ao reportar ocorrência.", "error");
}
};
const handleSaveFatura = async (e) => {
e.preventDefault();
if (!formData.moradorId || !formData.categoria || !formData.valor || !formData.dataVencimento) {
showNotification("Preencha todos os campos obrigatórios.", "error");
return;
}
try {
// Comparação segura entre IDs (firebase keys são strings)
const morador = residents.find(r => String(r.id) === String(formData.moradorId));
if (!morador) {
showNotification("Morador seleccionado não encontrado.", "error");
return;
}
const valor = Number(formData.valor);
const newFaturaRef = push(ref(db, 'faturas'));
const newFatura = {
moradorId: morador.id,
nomeMorador: morador.name,
fracao: morador.unit,
categoria: formData.categoria,
valor: valor,
dataVencimento: formData.dataVencimento,
status: DB_STATUS.PENDENTE,
dataEmissao: new Date().toISOString().split('T')[0]
};
await set(newFaturaRef, newFatura);
setFaturas(prev => [{ id: newFaturaRef.key, ...newFatura }, ...prev].sort(sortByVencimentoDesc));
const newPending = (Number(morador.pending) || 0) + valor;
await update(ref(db, `condominos/${morador.id}`), {
pending: newPending,
status: DB_STATUS.PENDENTE
});
setResidents(prev => prev.map(r => r.id === morador.id ? { ...r, pending: newPending, status: DB_STATUS.PENDENTE } : r));
sendSystemNotification(`Foi emitida uma nova fatura no valor de ${valor.toFixed(2)}€ (Categoria: ${formData.categoria})`, 'warning', morador.id);
sendSystemNotification(`Fatura de ${valor.toFixed(2)}€ emitida para ${morador.name} (${morador.unit})`, 'info', 'admin');
showNotification(`Fatura de ${valor.toFixed(2)}€ emitida para ${morador.name}`);
handleCloseModal();
} catch (error) {
console.error("Erro ao emitir fatura:", error);
showNotification("Erro ao emitir fatura.", "error");
}
};
const handlePayFatura = async (fatura) => {
try {
await set(ref(db, `faturas/${fatura.id}/status`), DB_STATUS.PAGO);
setFaturas(prev => prev.map(f => f.id === fatura.id ? { ...f, status: DB_STATUS.PAGO } : f));
const morador = residents.find(r => r.id === fatura.moradorId);
if (morador) {
let newPending = (Number(morador.pending) || 0) - Number(fatura.valor);
if (newPending <= 0.01) newPending = 0;
const newStatus = newPending === 0 ? DB_STATUS.PAGO : DB_STATUS.PENDENTE;
await update(ref(db, `condominos/${morador.id}`), {
pending: newPending,
status: newStatus
});
setResidents(prev => prev.map(r => r.id === morador.id ? { ...r, pending: newPending, status: newStatus } : r));
}
sendSystemNotification(`O pagamento da sua fatura de ${fatura.categoria} foi concluído!`, 'success', fatura.moradorId);
sendSystemNotification(`Pagamento registado para a fatura de ${fatura.categoria} da fração ${morador?.unit || fatura.fracao}.`, 'success', 'admin');
showNotification("Pagamento efetuado com sucesso!", "success");
} catch (error) {
console.error("Erro ao pagar fatura:", error);
showNotification("Erro ao processar pagamento.", "error");
}
};
const handleApproveFatura = async (fatura) => {
try {
await set(ref(db, `faturas/${fatura.id}/status`), DB_STATUS.PAGO);
setFaturas(prev => prev.map(f => f.id === fatura.id ? { ...f, status: DB_STATUS.PAGO } : f));
const morador = residents.find(r => r.id === fatura.moradorId);
if (morador) {
let newPending = (Number(morador.pending) || 0) - Number(fatura.valor);
if (newPending <= 0.01) newPending = 0;
const newStatus = newPending === 0 ? DB_STATUS.PAGO : DB_STATUS.PENDENTE;
await update(ref(db, `condominos/${morador.id}`), {
pending: newPending,
status: newStatus
});
setResidents(prev => prev.map(r => r.id === morador.id ? { ...r, pending: newPending, status: newStatus } : r));
}
sendSystemNotification(`O seu pagamento da fatura de ${fatura.categoria} foi aprovado!`, 'success', fatura.moradorId);
sendSystemNotification(`Pagamento aprovado para a fatura de ${fatura.categoria} da fração ${morador?.unit || fatura.fracao}.`, 'success', 'admin');
showNotification("Pagamento aprovado com sucesso!", "success");
} catch (error) {
console.error("Erro ao aprovar fatura:", error);
showNotification("Erro ao processar aprovação.", "error");
}
};
const handleResolveIssue = async (id) => {
try {
const issue = issues.find(i => i.id === id);
if (issue) {
const resolvedIssue = { ...issue, status: DB_STATUS.RESOLVIDO };
await set(ref(db, `manutencao/${id}`), resolvedIssue);
setIssues(prev => prev.map(i => i.id === id ? resolvedIssue : i));
sendSystemNotification(`A manutenção "${issue.title}" foi concluída com sucesso.`, 'success', 'todos');
showNotification(t('ocorr_ncia_resolvida_com_sucesso'));
}
} catch (error) {
console.error("Erro ao resolver ocorrência:", error);
showNotification("Erro ao resolver ocorrência.", "error");
}
};
const handleSaveBooking = async (e) => {
e.preventDefault();
if (!formData.resident || !formData.date || !formData.time) {
showNotification("Preencha todos os campos obrigatórios.", "error");
return;
}
try {
const facilityNames = { 'gym': 'Ginásio', 'hall': t('sal_o_de_festas'), 'park': t('parque_de_jogos') };
const bookingData = {
...formData,
facilityName: facilityNames[formData.facility],
status: DB_STATUS.CONFIRMADO,
moradorId: currentUserId
};
const newBookingRef = push(ref(db, 'reservas'));
await set(newBookingRef, bookingData);
setBookings(prev => [{ id: newBookingRef.key, ...bookingData }, ...prev].sort(sortByDateDesc));
if (bookingData.cost > 0) {
const newIncomeRef = push(ref(db, 'financas'));
const newIncome = {
type: 'income',
category: `Reserva: ${bookingData.facilityName}`,
date: bookingData.date,
amount: bookingData.cost,
desc: `Reserva por ${bookingData.resident}`
};
await set(newIncomeRef, newIncome);
setFinances(prev => [{ id: newIncomeRef.key, ...newIncome }, ...prev].sort(sortByDateDesc));
}
sendSystemNotification(`Nova reserva: ${bookingData.facilityName} a ${bookingData.date}`, 'info', 'admin');
if (userRole !== 'admin') {
sendSystemNotification(`A sua reserva para ${bookingData.facilityName} foi confirmada.`, 'success', currentUserId);
}
showNotification(`Reserva confirmada para ${bookingData.facilityName}`);
handleCloseModal();
} catch (error) {
console.error("Erro ao criar reserva:", error);
showNotification("Erro ao criar reserva.", "error");
}
};
const handleGenerateInvoice = async (resident) => {
if (resident.pending <= 0) {
showNotification(`Não há dívidas para a fração ${resident.unit}`, 'warning');
return;
}
try {
const newInvoiceRef = push(ref(db, 'faturacao'));
const newInvoice = {
residentId: resident.id,
unit: resident.unit,
name: resident.name,
amount: Number(resident.pending),
date: new Date().toISOString().split('T')[0],
status: 'Emitida'
};
await set(newInvoiceRef, newInvoice);
setInvoices(prev => [{ id: newInvoiceRef.key, ...newInvoice }, ...prev].sort(sortByDateDesc));
sendSystemNotification(`Foi emitida uma nova fatura instantânea no valor de ${Number(resident.pending).toFixed(2)}`, 'warning', resident.id);
sendSystemNotification(`Fatura instantânea gerada para a fração ${resident.unit} no valor de ${Number(resident.pending).toFixed(2)}`, 'info', 'admin');
showNotification(`Fatura instantânea gerada para a fração ${resident.unit}`, 'success');
} catch (error) {
console.error("Erro ao faturar:", error);
showNotification("Erro ao gerar fatura.", "error");
}
};
const DashboardView = () => (
<div className="space-y-6 animate-fade-in">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{userRole === 'admin' ? (
<Card title={t('saldo_dispon_vel')} value={`${balance.toFixed(2)}`} icon={Wallet} trend={balance >= 0 ? 'up' : 'down'} trendValue="Atual" color="bg-blue-500" />
) : (
<Card title={t('as_minhas_quotas')} value="Em Dia" icon={CheckCircle} trend="up" trendValue={t('pago')} color="bg-green-500" subtitle={t('sem_valores_pendentes')} />
)}
<Card title={t('reservas_m_s')} value={bookings.length} icon={Calendar} trend="up" trendValue="+2" color="bg-purple-500" subtitle={t('total_agendado')} />
<Card title={t('manuten_es_ativas')} value={activeIssuesCount} icon={Wrench} trend="up" trendValue="Novas" color="bg-orange-500" subtitle={t('em_resolu_o')} />
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border flex flex-col h-full transition-colors">
<div className="p-6 border-b border-slate-100 dark:border-dark-border flex justify-between items-center">
<h3 className="font-bold text-lg text-slate-800 dark:text-white">{t('pr_ximas_reservas')}</h3>
<button className="text-blue-600 dark:text-blue-400 text-sm font-medium" onClick={() => setActiveTab('all_bookings')}>{t('ver_todas_as_reservas')}</button>
</div>
<div className="p-4 space-y-3">
{bookings.slice(0, 4).map(booking => (
<div key={booking.id} className="flex items-center justify-between p-3 border border-slate-100 dark:border-dark-border rounded-lg hover:bg-slate-50 dark:hover:bg-dark-card transition-colors">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${booking.facility === 'gym' ? 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400' : booking.facility === 'hall' ? 'bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400' : 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400'}`}>
{booking.facility === 'gym' ? <Dumbbell size={18} /> : booking.facility === 'hall' ? <PartyPopper size={18} /> : <Trophy size={18} />}
</div>
<div>
<p className="font-bold text-sm text-slate-800 dark:text-slate-200">{booking.facilityName}</p>
<p className="text-xs text-slate-500 dark:text-slate-400">{booking.date} {booking.time}</p>
</div>
</div>
<div className="text-right">
<p className="text-sm font-medium text-slate-700 dark:text-slate-300">{booking.resident}</p>
<Badge status={booking.status} />
</div>
</div>
))}
</div>
</div>
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border flex flex-col h-full transition-colors">
<div className="p-6 border-b border-slate-100 dark:border-dark-border flex justify-between items-center">
<h3 className="font-bold text-lg text-slate-800 dark:text-white">{t('quadro_de_avisos')}</h3>
<button className="text-sm text-blue-600 dark:text-blue-400" onClick={() => setActiveTab('maintenance')}>{userRole === 'admin' ? 'Gerir' : 'Ver Ocorrências'}</button>
</div>
<div className="p-4 space-y-3 overflow-y-auto max-h-[350px]">
{issues.slice(0, 3).map((issue) => (
<div key={issue.id} className="border border-slate-100 dark:border-dark-border rounded-lg p-4 bg-slate-50 dark:bg-dark-card transition-colors">
<div className="flex justify-between mb-2">
<Badge status={issue.priority} />
<span className="text-xs text-slate-400 dark:text-slate-500">{issue.date}</span>
</div>
<h4 className="font-semibold text-slate-800 dark:text-slate-200">{issue.title}</h4>
<p className="text-xs text-slate-500 dark:text-dark-mute mt-1">{issue.location}</p>
</div>
))}
</div>
</div>
</div>
</div>
);
const BookingView = ({ facilityType, title, icon: Icon, description, priceInfo, color }) => {
const facilityBookings = bookings.filter(b => b.facility === facilityType);
return (
<div className="animate-fade-in space-y-6">
<div className={`bg-white dark:bg-dark-surface p-6 rounded-xl shadow-sm border border-slate-100 dark:border-dark-border flex flex-col md:flex-row gap-6 items-center ${color}`}>
<div className="bg-white/20 p-6 rounded-2xl backdrop-blur-sm text-white">
<Icon size={48} />
</div>
<div className="flex-1 text-white">
<h2 className="text-2xl font-bold">{title}</h2>
<p className="opacity-90 mt-1">{description}</p>
<div className="mt-4 flex gap-4 text-sm font-medium bg-black/10 w-fit px-4 py-2 rounded-lg">
<span className="flex items-center gap-2"><Clock size={16} /> Horário: 08:00 - 22:00</span>
<span className="flex items-center gap-2"><Wallet size={16} /> {priceInfo}</span>
</div>
</div>
<button
onClick={() => handleOpenModal('booking', null, facilityType)}
className="bg-white text-slate-900 px-6 py-3 rounded-lg font-bold hover:bg-slate-50 transition-colors shadow-lg flex items-center gap-2"
>
<Plus size={20} /> Reservar Agora
</button>
</div>
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border p-6 transition-colors">
<h3 className="font-bold text-lg text-slate-800 dark:text-white mb-4">{t('agenda_de_reservas')}</h3>
{facilityBookings.length === 0 ? (
<div className="text-center py-12 text-slate-400 dark:text-dark-mute border-2 border-dashed border-slate-200 dark:border-dark-border rounded-xl">
<Calendar size={48} className="mx-auto mb-2 opacity-20" />
<p>Sem reservas agendadas para este espaço.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{facilityBookings.map(booking => (
<div key={booking.id} className="border border-slate-200 dark:border-dark-border rounded-lg p-4 hover:shadow-md transition-all bg-white dark:bg-dark-card relative overflow-hidden group">
<div className={`absolute top-0 left-0 w-1 h-full ${facilityType === 'gym' ? 'bg-blue-500' : facilityType === 'hall' ? 'bg-purple-500' : 'bg-green-500'}`}></div>
<div className="flex justify-between items-start mb-2 pl-2">
<span className="font-mono text-sm font-bold text-slate-500 dark:text-slate-400">{booking.date}</span>
<Badge status={booking.status} />
</div>
<h4 className="font-bold text-slate-800 dark:text-white text-lg pl-2">{booking.time}</h4>
<p className="text-sm text-slate-600 dark:text-dark-mute pl-2 mt-1 flex items-center gap-2">
<Users size={14} /> {booking.resident}
</p>
{booking.cost > 0 && (
<div className="mt-3 pl-2 pt-3 border-t border-slate-100 dark:border-dark-border text-right">
<span className="text-xs font-medium text-slate-400 uppercase">Custo:</span>
<span className="ml-2 font-bold text-slate-800 dark:text-white">{booking.cost}</span>
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
);
};
const MapView = () => {
const [activePoint, setActivePoint] = useState(null);
const [espacos, setEspacos] = useState([]);
const [route, setRoute] = useState(null);
const [isLocating, setIsLocating] = useState(false);
const IconMap = { Building2, ShoppingCart, Store, HeartPulse, Info, MapPin, Trophy, Dumbbell, PartyPopper, Waves, Home, Anchor, Fuel };
useEffect(() => {
const defaultEspacos = {
'bloco-a': { nome: 'Bloco A', tipo: 'Residencial', descricao: '10 andares • 20 Frações', icone: 'Building2', color: 'text-orange-500', bg: 'bg-orange-100 dark:bg-orange-900/40', border: 'border-orange-300 dark:border-orange-700/50', cx: 15, cy: 15, w: 20, h: 28, canBook: false, latitude: 38.7225, longitude: -9.1398 },
'moradia-1': { nome: 'Moradia 1', tipo: 'Residencial', descricao: 'Moradia T3', icone: 'Home', color: 'text-teal-600', bg: 'bg-teal-100 dark:bg-teal-900/40', border: 'border-teal-300 dark:border-teal-700/50', cx: 12, cy: 40, w: 12, h: 14, canBook: false, latitude: 38.7220, longitude: -9.1396 },
'moradia-2': { nome: 'Moradia 2', tipo: 'Residencial', descricao: 'Moradia T4', icone: 'Home', color: 'text-teal-600', bg: 'bg-teal-100 dark:bg-teal-900/40', border: 'border-teal-300 dark:border-teal-700/50', cx: 35, cy: 10, w: 12, h: 14, canBook: false, latitude: 38.7228, longitude: -9.1392 },
'moradia-3': { nome: 'Moradia 3', tipo: 'Residencial', descricao: 'Moradia T3', icone: 'Home', color: 'text-teal-600', bg: 'bg-teal-100 dark:bg-teal-900/40', border: 'border-teal-300 dark:border-teal-700/50', cx: 45, cy: 35, w: 12, h: 14, canBook: false, latitude: 38.7222, longitude: -9.1388 },
'moradia-4': { nome: 'Moradia 4', tipo: 'Residencial', descricao: 'Moradia T4', icone: 'Home', color: 'text-teal-600', bg: 'bg-teal-100 dark:bg-teal-900/40', border: 'border-teal-300 dark:border-teal-700/50', cx: 55, cy: 80, w: 12, h: 14, canBook: false, latitude: 38.7210, longitude: -9.1382 },
'moradia-5': { nome: 'Moradia 5', tipo: 'Residencial', descricao: 'Moradia T3', icone: 'Home', color: 'text-teal-600', bg: 'bg-teal-100 dark:bg-teal-900/40', border: 'border-teal-300 dark:border-teal-700/50', cx: 25, cy: 60, w: 12, h: 14, canBook: false, latitude: 38.7214, longitude: -9.1394 },
'moradia-6': { nome: 'Moradia 6', tipo: 'Residencial', descricao: 'Moradia T4', icone: 'Home', color: 'text-teal-600', bg: 'bg-teal-100 dark:bg-teal-900/40', border: 'border-teal-300 dark:border-teal-700/50', cx: 65, cy: 30, w: 12, h: 14, canBook: false, latitude: 38.7224, longitude: -9.1378 },
'moradia-7': { nome: 'Moradia 7', tipo: 'Residencial', descricao: 'Moradia T3', icone: 'Home', color: 'text-teal-600', bg: 'bg-teal-100 dark:bg-teal-900/40', border: 'border-teal-300 dark:border-teal-700/50', cx: 8, cy: 80, w: 12, h: 14, canBook: false, latitude: 38.7216, longitude: -9.1399 },
'moradia-8': { nome: 'Moradia 8', tipo: 'Residencial', descricao: 'Moradia T4', icone: 'Home', color: 'text-teal-600', bg: 'bg-teal-100 dark:bg-teal-900/40', border: 'border-teal-300 dark:border-teal-700/50', cx: 20, cy: 90, w: 12, h: 14, canBook: false, latitude: 38.7214, longitude: -9.1396 },
'mercado-1': { nome: 'Supermercado', tipo: 'Comércio', descricao: 'Bens primeira necessidade', icone: 'ShoppingCart', color: 'text-amber-600', bg: 'bg-amber-100 dark:bg-amber-900/40', border: 'border-amber-300 dark:border-amber-700/50', cx: 40, cy: 50, w: 14, h: 16, canBook: false, latitude: 38.7218, longitude: -9.1389 },
'mercado-2': { nome: 'Cafetaria', tipo: 'Comércio', descricao: 'Mercearia e cafetaria', icone: 'Store', color: 'text-amber-600', bg: 'bg-amber-100 dark:bg-amber-900/40', border: 'border-amber-300 dark:border-amber-700/50', cx: 75, cy: 65, w: 12, h: 14, canBook: false, latitude: 38.7213, longitude: -9.1372 },
'medico': { nome: 'Clínica', tipo: 'Serviços', descricao: 'Saúde e Bem-estar', icone: 'HeartPulse', color: 'text-red-600', bg: 'bg-red-100 dark:bg-red-900/40', border: 'border-red-300 dark:border-red-700/50', cx: 35, cy: 90, w: 14, h: 16, canBook: false, latitude: 38.7208, longitude: -9.1392 },
'reception': { nome: 'Portaria Principal', tipo: 'Serviços', descricao: 'Segurança 24h', icone: 'Info', color: 'text-slate-700 dark:text-slate-300', bg: 'bg-slate-200 dark:bg-slate-700', border: 'border-slate-400 dark:border-slate-500', cx: 8, cy: 50, w: 10, h: 10, isRound: true, canBook: false, latitude: 38.7219, longitude: -9.1400 },
'pool': { nome: 'Piscina', tipo: 'Lazer', descricao: 'Piscina exterior', icone: 'MapPin', color: 'text-teal-600', bg: 'bg-teal-100 dark:bg-teal-900/40', border: 'border-teal-300 dark:border-teal-700/50', cx: 60, cy: 55, w: 16, h: 18, canBook: false, latitude: 38.7215, longitude: -9.1380 },
'park': { nome: 'Parque', tipo: 'Lazer', descricao: 'Parque de jogos', icone: 'Trophy', color: 'text-green-600', bg: 'bg-green-100 dark:bg-green-900/40', border: 'border-green-300 dark:border-green-700/50', cx: 50, cy: 15, w: 18, h: 22, canBook: true, bookId: 'park', latitude: 38.7228, longitude: -9.1385 },
'gym': { nome: 'Ginásio', tipo: 'Lazer', descricao: 'Cardio e Força', icone: 'Dumbbell', color: 'text-blue-600', bg: 'bg-blue-100 dark:bg-blue-900/40', border: 'border-blue-300 dark:border-blue-700/50', cx: 75, cy: 85, w: 14, h: 16, canBook: true, bookId: 'gym', latitude: 38.7209, longitude: -9.1374 },
'hall': { nome: 'Salão Festas', type: 'Lazer', descricao: 'Capacidade 50 px', icone: 'PartyPopper', color: 'text-purple-600', bg: 'bg-purple-100 dark:bg-purple-900/40', border: 'border-purple-300 dark:border-purple-700/50', cx: 65, cy: 15, w: 14, h: 16, canBook: true, bookId: 'hall', latitude: 38.7205, longitude: -9.1398 },
'marina': { nome: 'Aluguer Barcos', tipo: 'Náutica', descricao: 'Barcos e motas de água', icone: 'Anchor', color: 'text-cyan-700 dark:text-cyan-300', bg: 'bg-cyan-100 dark:bg-cyan-900/40', border: 'border-cyan-300 dark:border-cyan-700/50', cx: 86, cy: 35, w: 12, h: 14, canBook: true, bookId: 'marina', latitude: 38.7222, longitude: -9.1355 },
'fuel': { nome: 'Bomba Náutica', tipo: 'Náutica', descricao: 'Abastecimento', icone: 'Fuel', color: 'text-red-500', bg: 'bg-red-100 dark:bg-red-900/40', border: 'border-red-300 dark:border-red-700/50', cx: 86, cy: 50, w: 10, h: 10, isRound: true, canBook: false, latitude: 38.7219, longitude: -9.1355 },
'deck': { nome: 'Deque', tipo: 'Lazer', descricao: 'Lazer', icone: 'Waves', color: 'text-cyan-700 dark:text-cyan-300', bg: 'bg-cyan-100 dark:bg-cyan-900/40', border: 'border-cyan-300 dark:border-cyan-700/50', cx: 88, cy: 75, w: 10, h: 25, canBook: false, latitude: 38.7212, longitude: -9.1350 },
};
const espacosRef = ref(db, 'espacos');
const unsub = onValue(espacosRef, (snapshot) => {
if (snapshot.exists()) {
const data = snapshot.val();
if (!data['moradia-8']) {
set(ref(db, 'espacos'), defaultEspacos).catch(console.error);
const loadedEspacos = Object.keys(defaultEspacos).map(key => ({ id: key, ...defaultEspacos[key] }));
setEspacos(loadedEspacos);
} else {
const loadedEspacos = Object.keys(data).map(key => ({ id: key, ...data[key] }));
setEspacos(loadedEspacos);
}
} else {
// Seed inicial da base de dados se estiver vazia
set(ref(db, 'espacos'), defaultEspacos).catch(console.error);
}
});
return () => unsub();
}, []);
const getDistance = (lat1, lon1, lat2, lon2) => {
const R = 6371; // Raio da Terra em km
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon/2) * Math.sin(dLon/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c; // em km
};
const handleRoute = (espaco) => {
setIsLocating(true);
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(pos) => {
const userLat = pos.coords.latitude;
const userLng = pos.coords.longitude;
const distKm = getDistance(userLat, userLng, espaco.latitude, espaco.longitude);
// Mapeamento da localização GPS do utilizador para as coordenadas visuais (x,y) do mapa
const minLat = 38.7205; const maxLat = 38.7230;
const minLng = -9.1405; const maxLng = -9.1350;
let userX = ((userLng - minLng) / (maxLng - minLng)) * 100;
let userY = ((maxLat - userLat) / (maxLat - minLat)) * 100;
// Se estiver fora do condomínio (> 5km), coloca o utilizador na entrada principal
if (distKm > 5) {
userX = 48; // Receção / Portaria
userY = 95; // Entrada
} else {
userX = Math.max(5, Math.min(95, userX));
userY = Math.max(5, Math.min(95, userY));
}
setRoute({
active: true,
targetId: espaco.id,
distance: distKm * 1000,
walkTime: Math.max(1, Math.ceil((distKm / 5) * 60)), // 5 km/h a pé
driveTime: Math.max(1, Math.ceil((distKm / 30) * 60)), // 30 km/h de carro
userX,
userY
});
setIsLocating(false);
},
(error) => {
console.error("Erro de geolocalização:", error);
showNotification("Não foi possível obter a sua localização. Verifique as permissões do browser.", "error");
setIsLocating(false);
}
);
} else {
showNotification("Geolocalização não é suportada por este browser.", "error");
setIsLocating(false);
}
};
return (
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border overflow-hidden animate-fade-in flex flex-col h-full transition-colors">
<div className="p-6 border-b border-slate-100 dark:border-dark-border flex justify-between items-center">
<div>
<h3 className="font-bold text-lg text-slate-800 dark:text-white">{t('navega_o_inteligente')}</h3>
<p className="text-sm text-slate-500 dark:text-dark-mute">{t('explore_e_encontre_rotas_no_condom_nio')}</p>
</div>
<div className="flex flex-wrap gap-3">
<span className="flex items-center gap-1 text-xs font-medium bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 px-2 py-1 rounded">{t('residencial')}</span>
<span className="flex items-center gap-1 text-xs font-medium bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 px-2 py-1 rounded">{t('com_rcio')}</span>
<span className="flex items-center gap-1 text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 px-2 py-1 rounded">{t('lazer')}</span>
<span className="flex items-center gap-1 text-xs font-medium bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 px-2 py-1 rounded">{t('servi_os')}</span>
</div>
</div>
<div className="flex flex-col lg:flex-row flex-1 min-h-[700px]">
{/* Área do Mapa */}
<div className="flex-1 p-6 bg-slate-50 dark:bg-dark-bg flex items-center justify-center relative overflow-hidden">
<div className="relative w-full h-full max-h-[800px] min-h-[600px] bg-[#eef8f2] dark:bg-[#1a2e23] border-4 border-slate-300 dark:border-dark-border rounded-2xl shadow-2xl p-4 transition-all duration-500 overflow-hidden">
<div className="absolute inset-0 opacity-10 bg-[linear-gradient(0deg,transparent_24%,#000_25%,#000_26%,transparent_27%,transparent_74%,#000_75%,#000_76%,transparent_77%,transparent),linear-gradient(90deg,transparent_24%,#000_25%,#000_26%,transparent_27%,transparent_74%,#000_75%,#000_76%,transparent_77%,transparent)] bg-[length:40px_40px]"></div>
{/* Visual River */}
<div className="absolute right-0 top-0 bottom-0 w-[12%] bg-blue-400/30 dark:bg-blue-600/30 border-l-8 border-blue-300/50 dark:border-blue-700/50 flex items-center justify-center overflow-hidden pointer-events-none">
<div className="text-blue-600/40 dark:text-blue-300/30 font-black text-4xl rotate-90 whitespace-nowrap tracking-[1em]">{t('rio_tejo')}</div>
</div>
{/* Ruas e Caminhos em SVG */}
<svg viewBox="0 0 100 100" preserveAspectRatio="none" className="absolute inset-0 w-full h-full pointer-events-none z-0" style={{ overflow: 'visible' }}>
{/* Via Central */}
<rect x="0" y="46" width="88" height="8" fill="#cbd5e1" className="dark:fill-slate-700" />
<line x1="0" y1="50" x2="88" y2="50" stroke="#f8fafc" strokeWidth="0.5" strokeDasharray="2,2" className="dark:stroke-slate-400" />
{/* Caminhos Secundários Orgânicos */}
<g stroke="#cbd5e1" strokeWidth="3" className="dark:stroke-slate-700" strokeLinecap="round" strokeLinejoin="round" fill="none">
{/* Top Arch */}
<path d="M 25 50 C 25 15, 40 5, 55 5 C 75 5, 80 20, 80 30 C 80 40, 85 45, 85 50" />
{/* Middle Top Winding */}
<path d="M 40 50 C 40 30, 50 25, 60 25 C 70 25, 75 30, 80 30" />
{/* Bottom Winding S-shape */}
<path d="M 18 50 C 18 85, 45 85, 45 65 C 45 45, 65 95, 85 50" />
{/* Ramos de Ligação (Atalhos) */}
<path d="M 15 15 L 28 15" strokeWidth="1.5" /> {/* Bloco A */}
<path d="M 8 80 L 27 80" strokeWidth="1.5" /> {/* Moradia 7 */}
<path d="M 20 90 L 20 60" strokeWidth="1.5" /> {/* Moradia 8 */}
<path d="M 12 40 L 12 50" strokeWidth="1.5" /> {/* Moradia 1 */}
<path d="M 35 10 L 35 15" strokeWidth="1.5" /> {/* Moradia 2 */}
<path d="M 45 35 L 45 29" strokeWidth="1.5" /> {/* Moradia 3 */}
<path d="M 55 80 L 55 64" strokeWidth="1.5" /> {/* Moradia 4 */}
<path d="M 25 60 L 25 78" strokeWidth="1.5" /> {/* Moradia 5 */}
<path d="M 65 30 L 65 26" strokeWidth="1.5" /> {/* Moradia 6 */}
<path d="M 75 65 L 75 68" strokeWidth="1.5" /> {/* Cafetaria */}
<path d="M 35 90 L 35 72" strokeWidth="1.5" /> {/* Clinica */}
<path d="M 60 55 L 60 68" strokeWidth="1.5" /> {/* Piscina */}
<path d="M 50 15 L 50 5" strokeWidth="1.5" /> {/* Parque */}
<path d="M 75 85 L 72 65" strokeWidth="1.5" /> {/* Ginasio */}
<path d="M 65 15 L 65 28" strokeWidth="1.5" /> {/* Salao */}
<path d="M 86 35 L 80 35" strokeWidth="1.5" /> {/* Barcos */}
<path d="M 88 75 L 78 60" strokeWidth="1.5" /> {/* Deque */}
</g>
<g stroke="#f8fafc" strokeWidth="0.5" strokeDasharray="1,1" className="dark:stroke-slate-400" strokeLinecap="round" strokeLinejoin="round" fill="none">
{/* Centros das Vias Orgânicas */}
<path d="M 25 50 C 25 15, 40 5, 55 5 C 75 5, 80 20, 80 30 C 80 40, 85 45, 85 50" />
<path d="M 40 50 C 40 30, 50 25, 60 25 C 70 25, 75 30, 80 30" />
<path d="M 18 50 C 18 85, 45 85, 45 65 C 45 45, 65 95, 85 50" />
</g>
</svg>
<div className="absolute top-[50%] left-0 w-[88%] h-12 transform -translate-y-1/2 flex items-center justify-center text-slate-500 dark:text-slate-300 font-black tracking-[0.8em] opacity-60 text-xs sm:text-sm pointer-events-none z-0">{t('via_central')}</div>
{/* Árvores Decorativas (Espalhadas) */}
{[
{x: 5, y: 5}, {x: 12, y: 8}, {x: 25, y: 5}, {x: 45, y: 8}, {x: 60, y: 5}, {x: 80, y: 8},
{x: 5, y: 90}, {x: 12, y: 85}, {x: 25, y: 90}, {x: 45, y: 88}, {x: 60, y: 92}, {x: 80, y: 85},
{x: 25, y: 35}, {x: 45, y: 45}, {x: 80, y: 40},
{x: 25, y: 65}, {x: 45, y: 60}, {x: 80, y: 60},
{x: 50, y: 30}, {x: 55, y: 65}, {x: 10, y: 70}
].map((tree, i) => (
<div key={`tree-${i}`} className="absolute w-6 h-6 bg-green-600/50 dark:bg-green-800/50 rounded-full blur-[2px] shadow-lg pointer-events-none z-0" style={{ left: `${tree.x}%`, top: `${tree.y}%` }}></div>
))}
{/* SVG Route Overlay */}
{route && route.targetId && (
<svg className="absolute inset-0 w-full h-full pointer-events-none z-30" style={{ overflow: 'visible' }}>
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#3b82f6" />
</marker>
</defs>
{espacos.filter(e => e.id === route.targetId).map(target => {
const targetCX = target.cx !== undefined ? target.cx : target.x + (target.w||10) / 2;
const targetCY = target.cy !== undefined ? target.cy : target.y + (target.h||10) / 2;
return (
<path
key="route-path"
d={`M ${route.userX}% ${route.userY}% Q 50% 50% ${targetCX}% ${targetCY}%`}
fill="none"
stroke="#3b82f6"
strokeWidth="4"
strokeDasharray="8, 8"
className="animate-[dash_1s_linear_infinite]"
markerEnd="url(#arrowhead)"
/>
);
})}
<circle cx={`${route.userX}%`} cy={`${route.userY}%`} r="6" fill="#3b82f6" className="animate-ping" />
<circle cx={`${route.userX}%`} cy={`${route.userY}%`} r="4" fill="#1e40af" />
<style>{`
@keyframes dash { to { stroke-dashoffset: -16; } }
`}</style>
</svg>
)}
{espacos.map(loc => {
const IconComp = IconMap[loc.icone] || MapPin;
// Reduzir o tamanho em 25% (scale = 0.75) mantendo o centro
const scale = 0.75;
const w = (loc.w || 10) * scale;
const h = (loc.h || 10) * scale;
const cx = loc.cx !== undefined ? loc.cx : loc.x + (loc.w || 10) / 2;
const cy = loc.cy !== undefined ? loc.cy : loc.y + (loc.h || 10) / 2;
const x = cx - w / 2;
const y = cy - h / 2;
return (
<div
key={loc.id}
onClick={() => setActivePoint(loc.id)}
onMouseEnter={() => setActivePoint(loc.id)}
className={`absolute flex flex-col items-center justify-center cursor-pointer transition-all duration-300 border-2 ${loc.bg} ${loc.border} ${activePoint === loc.id ? 'scale-110 shadow-2xl z-20 ring-4 ring-blue-400/50' : 'hover:scale-105 shadow-md z-10'} ${loc.isRound ? 'rounded-full' : 'rounded-xl bg-white/90 dark:bg-dark-surface/90 backdrop-blur-sm'}`}
style={{ left: `${x}%`, top: `${y}%`, width: `${w}%`, height: `${h}%` }}
>
<IconComp size={loc.isRound ? 14 : 20} className={`${loc.color} ${activePoint === loc.id ? 'animate-bounce' : ''}`} />
{!loc.isRound && (
<span className={`font-black text-[9px] sm:text-[10px] mt-1 text-center px-1 text-slate-800 dark:text-slate-200 leading-tight drop-shadow-sm`}>
{loc.nome}
</span>
)}
</div>
);
})}
</div>
</div>
{/* Tabela lateral / Legenda */}
<div className="w-full lg:w-96 border-t lg:border-t-0 lg:border-l border-slate-200 dark:border-dark-border bg-white dark:bg-dark-surface overflow-y-auto flex flex-col h-[400px] lg:h-auto">
<div className="p-4 bg-slate-50 dark:bg-dark-card border-b border-slate-200 dark:border-dark-border sticky top-0 z-10">
<h4 className="font-bold text-slate-800 dark:text-white flex items-center gap-2">
<MapPin size={18} className="text-blue-600" /> Detalhes e Navegação
</h4>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">Selecione um ponto no mapa para ver rotas.</p>
</div>
<div className="flex-1 p-3 space-y-2">
{espacos.map(loc => {
const IconComp = IconMap[loc.icone] || MapPin;
return (
<div
key={`list-${loc.id}`}
onClick={() => setActivePoint(loc.id)}
className={`p-3 rounded-xl cursor-pointer transition-all border ${activePoint === loc.id ? 'bg-blue-50 border-blue-200 dark:bg-blue-900/20 dark:border-blue-800 shadow-md transform scale-[1.02]' : 'bg-white border-slate-100 hover:border-slate-300 dark:bg-dark-card dark:border-dark-border hover:bg-slate-50 dark:hover:bg-dark-bg'}`}
>
<div className="flex items-start gap-3">
<div className={`p-2.5 rounded-lg border ${loc.bg} ${loc.border}`}>
<IconComp size={20} className={loc.color} />
</div>
<div className="flex-1 pt-1">
<div className="flex justify-between items-start">
<h5 className="font-bold text-sm text-slate-800 dark:text-white">{loc.nome}</h5>
<span className={`text-[9px] uppercase font-bold px-2 py-0.5 rounded-full ${loc.tipo === t('residencial') ? 'bg-orange-100 text-orange-700' : loc.tipo === t('com_rcio') ? 'bg-amber-100 text-amber-700' : loc.tipo === t('lazer') ? 'bg-blue-100 text-blue-700' : 'bg-slate-100 text-slate-700'} dark:bg-opacity-20`}>{loc.tipo}</span>
</div>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">{loc.descricao}</p>
{activePoint === loc.id && (
<div className="mt-3 flex flex-col gap-2">
<button
onClick={(e) => { e.stopPropagation(); handleRoute(loc); }}
disabled={isLocating}
className="w-full text-xs bg-indigo-600 text-white px-3 py-2 rounded-lg hover:bg-indigo-700 transition-colors shadow-sm flex items-center justify-center gap-1.5 font-bold disabled:opacity-50"
>
<Navigation size={14} /> {isLocating ? 'A localizar...' : 'Navegar até aqui'}
</button>
{loc.canBook && (
<button
onClick={(e) => { e.stopPropagation(); handleOpenModal('booking', null, loc.bookId); }}
className="w-full text-xs bg-blue-600 text-white px-3 py-2 rounded-lg hover:bg-blue-700 transition-colors shadow-sm flex items-center justify-center gap-1.5 font-bold"
>
<Calendar size={14} /> Fazer Reserva
</button>
)}
{route && route.targetId === loc.id && (
<div className="mt-2 bg-indigo-50 dark:bg-indigo-900/30 p-3 rounded-lg border border-indigo-100 dark:border-indigo-800/50 animate-fade-in">
<h6 className="text-[10px] uppercase font-bold text-indigo-800 dark:text-indigo-300 mb-2 border-b border-indigo-200 dark:border-indigo-700 pb-1">{t('detalhes_da_rota')}</h6>
<div className="flex justify-between items-center text-xs text-slate-700 dark:text-slate-300">
<span className="flex items-center gap-1"><MapPin size={12} className="text-indigo-500"/> Distância:</span>
<span className="font-bold">{route.distance > 1000 ? (route.distance/1000).toFixed(1) + ' km' : Math.round(route.distance) + ' m'}</span>
</div>
<div className="flex justify-between items-center text-xs text-slate-700 dark:text-slate-300 mt-1.5">
<span className="flex items-center gap-1"><Info size={12} className="text-indigo-500"/> A :</span>
<span className="font-bold">{route.walkTime} min</span>
</div>
<div className="flex justify-between items-center text-xs text-slate-700 dark:text-slate-300 mt-1.5">
<span className="flex items-center gap-1"><Car size={12} className="text-indigo-500"/> De carro:</span>
<span className="font-bold">{route.driveTime} min</span>
</div>
</div>
)}
</div>
)}
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
</div>
);
};
const MaintenanceView = () => (
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border overflow-hidden animate-fade-in flex flex-col h-full transition-colors">
<div className="p-6 border-b border-slate-100 dark:border-dark-border flex justify-between items-center">
<div>
<h3 className="font-bold text-lg text-slate-800 dark:text-white">{t('manuten_o_e_ocorr_ncias')}</h3>
<p className="text-sm text-slate-500 dark:text-dark-mute">{t('gest_o_de_pedidos_e_repara_es')}</p>
</div>
{userRole === 'admin' ? (
<button
className="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 shadow-sm transition-colors cursor-default"
>
<CheckCircle size={18} /> Resolver Problemas
</button>
) : (
<button
onClick={() => handleOpenModal('issue')}
className="bg-orange-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-orange-700 flex items-center gap-2 shadow-sm transition-colors"
>
<Plus size={18} /> Reportar Problema
</button>
)}
</div>
<div className="flex-1 overflow-auto p-6 bg-slate-50 dark:bg-dark-bg">
{issues.length === 0 ? (
<div className="text-center py-12 text-slate-400 dark:text-dark-mute border-2 border-dashed border-slate-200 dark:border-dark-border rounded-xl">
<Wrench size={48} className="mx-auto mb-2 opacity-20" />
<p>Sem ocorrências registadas.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{issues.map(issue => (
<div key={issue.id} className="bg-white dark:bg-dark-surface rounded-lg shadow-sm border border-slate-200 dark:border-dark-border p-5 hover:shadow-md transition-shadow relative overflow-hidden">
<div className={`absolute top-0 left-0 w-1 h-full ${issue.priority === t('alta') ? 'bg-red-500' :
issue.priority === t('m_dia') ? 'bg-orange-500' : 'bg-blue-500'
}`}></div>
<div className="flex justify-between items-start mb-3 pl-2">
<Badge status={issue.status} />
<span className="text-xs text-slate-400 dark:text-slate-500 font-mono">{issue.date}</span>
</div>
<h4 className="font-bold text-slate-800 dark:text-white text-lg mb-1 pl-2">{issue.title}</h4>
<p className="text-sm text-slate-600 dark:text-dark-mute mb-4 pl-2 flex items-center gap-2">
<MapPin size={14} /> {issue.location}
</p>
<div className="pt-4 border-t border-slate-100 dark:border-dark-border flex justify-between items-center pl-2">
<span className={`text-xs font-bold px-2 py-1 rounded ${issue.priority === t('alta') ? 'bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400' :
issue.priority === t('m_dia') ? 'bg-orange-50 dark:bg-orange-900/20 text-orange-700 dark:text-orange-400' : 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400'
}`}>
Prioridade {issue.priority}
</span>
{userRole === 'admin' && !isResolvedStatus(issue.status) && (
<button
onClick={() => handleResolveIssue(issue.id)}
className="text-sm text-green-600 dark:text-green-400 font-medium hover:text-green-700 dark:hover:text-green-300 flex items-center gap-1"
>
<CheckCircle size={16} /> Resolver
</button>
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
);
const ProfileView = ({ theme, setTheme }) => {
const [activeSection, setActiveSection] = useState('personal');
const isMorador = userRole !== 'admin';
const [profileForm, setProfileForm] = useState({
name: 'A carregar...',
role: '...',
email: '',
contact: '',
address: ''
});
useEffect(() => {
if (isMorador) {
const currentUserData = residents.find(r => r.id === currentUserId) || {};
setProfileForm({
name: currentUserData.name || currentUserName || '',
role: `Fração ${currentUserData.unit || 'N/A'}`,
email: currentUserData.email || '',
contact: currentUserData.contact || '',
address: 'Morada do Condomínio',
photoUrl: currentUserData.photoUrl || ''
});
} else {
const adminRef = ref(db, 'configuracoes/admin_profile');
const unsub = onValue(adminRef, (snapshot) => {
if (snapshot.exists()) {
setProfileForm(snapshot.val());
} else {
setProfileForm({
name: 'Administrador do Condomínio',
role: 'Síndico / Gestor',
email: 'admin@mycondominium.pt',
contact: '+351 912 345 678',
address: 'Rua das Flores, nº 123, Escritório 2B'
});
}
});
return () => unsub();
}
}, [residents, currentUserId, userRole, currentUserName, isMorador]);
const handleChange = (field, value) => {
setProfileForm(prev => ({ ...prev, [field]: value }));
};
const [passwordData, setPasswordData] = useState({ current: '', new: '', confirm: '' });
const handlePasswordChange = (field, value) => {
setPasswordData(prev => ({ ...prev, [field]: value }));
};
const handleSavePassword = async () => {
if (passwordData.new !== passwordData.confirm) {
showNotification('As novas palavras-passe não coincidem.', 'error');
return;
}
if (passwordData.new.length < 4) {
showNotification('A nova palavra-passe deve ter pelo menos 4 caracteres.', 'error');
return;
}
if (isMorador) {
const currentUserData = residents.find(r => r.id === currentUserId);
const currentPassword = currentUserData.password || currentUserData.contact || '1234';
if (passwordData.current !== currentPassword) {
showNotification('A palavra-passe atual está incorreta.', 'error');
return;
}
try {
await set(ref(db, `condominos/${currentUserData.id}/password`), passwordData.new);
setResidents(prev => prev.map(r => r.id === currentUserData.id ? { ...r, password: passwordData.new } : r));
showNotification('Palavra-passe alterada com sucesso!', 'success');
setPasswordData({ current: '', new: '', confirm: '' });
sendSystemNotification('Um utilizador alterou a sua palavra-passe.', 'info', 'admin');
} catch (error) {
console.error("Erro ao alterar palavra-passe:", error);
showNotification('Erro ao alterar a palavra-passe.', 'error');
}
} else {
showNotification('A conta de administrador usa o Firebase Auth para gerir passwords.', 'info');
}
};
const handleSave = async () => {
if (isMorador) {
const currentUserData = residents.find(r => r.id === currentUserId);
if (currentUserData && currentUserData.id) {
try {
await set(ref(db, `condominos/${currentUserData.id}/email`), profileForm.email);
await set(ref(db, `condominos/${currentUserData.id}/contact`), profileForm.contact);
if (profileForm.photoUrl !== undefined) {
await set(ref(db, `condominos/${currentUserData.id}/photoUrl`), profileForm.photoUrl);
}
setResidents(prev => prev.map(r => r.id === currentUserData.id ? {
...r,
email: profileForm.email,
contact: profileForm.contact,
...(profileForm.photoUrl !== undefined ? { photoUrl: profileForm.photoUrl } : {})
} : r));
showNotification('Dados atualizados com sucesso!', 'success');
sendSystemNotification('Um utilizador atualizou os seus dados pessoais.', 'info', 'admin');
} catch (error) {
console.error("Erro ao guardar os dados:", error);
showNotification('Erro ao guardar os dados.', 'error');
}
}
} else {
try {
await set(ref(db, 'configuracoes/admin_profile'), profileForm);
showNotification('Alterações guardadas com sucesso!', 'success');
} catch (error) {
console.error("Erro ao guardar perfil admin:", error);
showNotification('Erro ao guardar as alterações.', 'error');
}
}
};
const fileInputRef = React.useRef(null);
const handleImageChange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
handleChange('photoUrl', reader.result);
};
reader.readAsDataURL(file);
}
};
return (
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border overflow-hidden animate-fade-in flex flex-col h-full transition-colors">
<div className="flex flex-col md:flex-row h-full">
{/* Profile Sidebar */}
<div className="w-full md:w-64 bg-slate-50 dark:bg-dark-bg border-r border-slate-100 dark:border-dark-border p-6 flex flex-col gap-2">
<div className="text-center mb-6">
<div
className="w-24 h-24 bg-blue-100 dark:bg-blue-900/30 rounded-full mx-auto flex items-center justify-center text-blue-600 dark:text-blue-400 font-bold text-3xl mb-3 border-4 border-white dark:border-dark-surface shadow-sm cursor-pointer relative group overflow-hidden transition-all"
onClick={() => fileInputRef.current && fileInputRef.current.click()}
>
{profileForm.photoUrl ? (
<img src={profileForm.photoUrl} alt="Perfil" className="w-full h-full object-cover" />
) : (
userRole === 'admin' ? 'AD' : 'MO'
)}
<div className="absolute inset-0 bg-black/50 hidden group-hover:flex items-center justify-center text-white transition-all">
<span className="text-xs font-medium">{t('alterar')}</span>
</div>
</div>
<input
type="file"
ref={fileInputRef}
onChange={handleImageChange}
accept="image/*"
className="hidden"
/>
<h3 className="font-bold text-slate-800 dark:text-white">{userRole === 'admin' ? 'Admin Condomínio' : 'Morador'}</h3>
<p className="text-xs text-slate-500 dark:text-dark-mute">{userRole === 'admin' ? 'Administrador Geral' : 'Residente'}</p>
</div>
<button
onClick={() => setActiveSection('personal')}
className={`text-left px-4 py-3 rounded-lg text-sm font-medium transition-colors flex items-center gap-3 ${activeSection === 'personal' ? 'bg-white dark:bg-dark-surface text-blue-600 dark:text-blue-400 shadow-sm' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-dark-card'}`}
>
<Users size={18} /> Dados Pessoais
</button>
<button
onClick={() => setActiveSection('security')}
className={`text-left px-4 py-3 rounded-lg text-sm font-medium transition-colors flex items-center gap-3 ${activeSection === 'security' ? 'bg-white dark:bg-dark-surface text-blue-600 dark:text-blue-400 shadow-sm' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-dark-card'}`}
>
<LogOut size={18} className="rotate-90" /> Segurança
</button>
{userRole === 'admin' && (
<button
onClick={() => setActiveSection('permissions')}
className={`text-left px-4 py-3 rounded-lg text-sm font-medium transition-colors flex items-center gap-3 ${activeSection === 'permissions' ? 'bg-white dark:bg-dark-surface text-blue-600 dark:text-blue-400 shadow-sm' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-dark-card'}`}
>
<CheckCircle size={18} /> Permissões
</button>
)}
<button
onClick={() => setActiveSection('settings')}
className={`text-left px-4 py-3 rounded-lg text-sm font-medium transition-colors flex items-center gap-3 ${activeSection === 'settings' ? 'bg-white dark:bg-dark-surface text-blue-600 dark:text-blue-400 shadow-sm' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-dark-card'}`}
>
<Wrench size={18} /> Preferências
</button>
</div>
{/* Profile Content */}
<div className="flex-1 p-8 overflow-y-auto">
{activeSection === 'personal' && (
<div className="animate-fade-in max-w-2xl">
<h3 className="text-lg font-bold text-slate-800 dark:text-white mb-6 pb-2 border-b border-slate-100 dark:border-dark-border">{t('dados_pessoais')}</h3>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<InputGroup label={t('nome_completo')} value={profileForm.name} onChange={(e) => handleChange('name', e.target.value)} disabled={isMorador} />
<InputGroup label={isMorador ? t('fra_o') : "Cargo"} value={profileForm.role} onChange={(e) => handleChange('role', e.target.value)} disabled={isMorador} />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<InputGroup label={t('email')} value={profileForm.email} onChange={(e) => handleChange('email', e.target.value)} type="email" />
<InputGroup label={t('telefone')} value={profileForm.contact} onChange={(e) => handleChange('contact', e.target.value)} />
</div>
<InputGroup label={isMorador ? "Morada" : "Morada (Sede)"} value={profileForm.address} onChange={(e) => handleChange('address', e.target.value)} disabled={isMorador} />
<div className="flex justify-end mt-6">
<button onClick={handleSave} className="bg-blue-600 text-white px-6 py-2 rounded-lg font-medium hover:bg-blue-700 shadow-sm transition-colors">
Guardar Alterações
</button>
</div>
</div>
</div>
)}
{activeSection === 'security' && (
<div className="animate-fade-in max-w-2xl">
<h3 className="text-lg font-bold text-slate-800 dark:text-white mb-6 pb-2 border-b border-slate-100 dark:border-dark-border">{t('seguran_a')}</h3>
<div className="bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-4 mb-6 flex items-start gap-3">
<AlertCircle className="text-orange-600 dark:text-orange-400 flex-shrink-0 mt-0.5" size={20} />
<div>
<h4 className="font-bold text-orange-800 dark:text-orange-300 text-sm">Autenticação de Dois Fatores (2FA)</h4>
<p className="text-xs text-orange-700 dark:text-orange-400 mt-1">Recomendamos ativar o 2FA para maior segurança da sua conta.</p>
<button onClick={() => {
showNotification('Autenticação de Dois Fatores ativada com sucesso!', 'success');
sendSystemNotification('Um utilizador ativou a autenticação 2FA.', 'success', 'admin');
}} className="text-orange-900 dark:text-orange-200 text-xs font-bold underline mt-2">{t('ativar_agora')}</button>
</div>
</div>
<div className="space-y-4">
<InputGroup label={t('palavra_passe_atual')} type="password" placeholder="••••••••" value={passwordData.current} onChange={(e) => handlePasswordChange('current', e.target.value)} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<InputGroup label={t('nova_palavra_passe')} type="password" placeholder={t('min_8_caracteres')} value={passwordData.new} onChange={(e) => handlePasswordChange('new', e.target.value)} />
<InputGroup label={t('confirmar_nova_palavra_passe')} type="password" placeholder={t('confirmar')} value={passwordData.confirm} onChange={(e) => handlePasswordChange('confirm', e.target.value)} />
</div>
<div className="flex justify-end mt-6">
<button onClick={handleSavePassword} className="bg-slate-800 dark:bg-slate-700 text-white px-6 py-2 rounded-lg font-medium hover:bg-slate-900 dark:hover:bg-slate-600 shadow-sm transition-colors">
Atualizar Segurança
</button>
</div>
</div>
</div>
)}
{activeSection === 'permissions' && (
<div className="animate-fade-in max-w-2xl">
<h3 className="text-lg font-bold text-slate-800 dark:text-white mb-6 pb-2 border-b border-slate-100 dark:border-dark-border">{t('n_vel_de_acesso')}</h3>
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4 mb-6 flex items-center gap-3">
<div className="bg-green-100 dark:bg-green-800/50 p-2 rounded-full">
<CheckCircle className="text-green-600 dark:text-green-400" size={24} />
</div>
<div>
<h4 className="font-bold text-green-800 dark:text-green-300">Acesso Total (Admin)</h4>
<p className="text-xs text-green-700 dark:text-green-400">Tem permissões totais para gerir condóminos, finanças e configurações.</p>
</div>
</div>
<div className="space-y-3">
<h4 className="text-sm font-bold text-slate-700 dark:text-slate-300 mb-2">Permissões Específicas:</h4>
{['Gerir Condóminos (Criar, Editar, Eliminar)', 'Gestão Financeira Completa', 'Moderação de Ocorrências', 'Configuração do Sistema', 'Gestão de Usuários'].map((perm, idx) => (
<div key={idx} className="flex items-center gap-3 p-3 border border-slate-100 dark:border-dark-border rounded-lg bg-slate-50 dark:bg-dark-card">
<div className="w-2 h-2 rounded-full bg-green-500"></div>
<span className="text-sm text-slate-600 dark:text-slate-300">{perm}</span>
</div>
))}
</div>
</div>
)}
{activeSection === 'settings' && (
<div className="animate-fade-in max-w-2xl">
<h3 className="text-lg font-bold text-slate-800 dark:text-white mb-6 pb-2 border-b border-slate-100 dark:border-dark-border">{t('prefer_ncias_da_aplica_o')}</h3>
<div className="space-y-6">
<div>
<h4 className="text-sm font-bold text-slate-700 dark:text-slate-300 mb-3">{t('notifica_es')}</h4>
<div className="space-y-2">
<label className="flex items-center gap-3 p-3 border border-slate-200 dark:border-dark-border rounded-lg cursor-pointer hover:bg-slate-50 dark:hover:bg-dark-card transition-colors">
<input type="checkbox" checked className="w-4 h-4 text-blue-600 rounded" readOnly />
<span className="text-sm text-slate-700 dark:text-slate-300">{t('alertas_por_email')}</span>
</label>
<label className="flex items-center gap-3 p-3 border border-slate-200 dark:border-dark-border rounded-lg cursor-pointer hover:bg-slate-50 dark:hover:bg-dark-card transition-colors">
<input type="checkbox" checked className="w-4 h-4 text-blue-600 rounded" readOnly />
<span className="text-sm text-slate-700 dark:text-slate-300">{t('notifica_es_push_no_navegador')}</span>
</label>
<label className="flex items-center gap-3 p-3 border border-slate-200 dark:border-dark-border rounded-lg cursor-pointer hover:bg-slate-50 dark:hover:bg-dark-card transition-colors">
<input type="checkbox" className="w-4 h-4 text-blue-600 rounded" readOnly />
<span className="text-sm text-slate-700 dark:text-slate-300">{t('relat_rios_semanais_autom_ticos')}</span>
</label>
</div>
</div>
<div>
<h4 className="text-sm font-bold text-slate-700 dark:text-slate-300 mb-3">{t('idioma_da_aplica_o')}</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[
{ code: 'pt', label: 'Português', flag: '🇵🇹' },
{ code: 'en', label: 'English', flag: '🇬🇧' },
{ code: 'es', label: 'Español', flag: '🇪🇸' },
{ code: 'fr', label: 'Français', flag: '🇫🇷' }
].map(lang => {
const match = document.cookie.match(/googtrans=\/pt\/([a-z]{2})/);
const activeLang = language;
const isActive = activeLang === lang.code;
return (
<div
key={lang.code}
onClick={() => {
if(lang.code === 'pt') {
document.cookie = "googtrans=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
document.cookie = "googtrans=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=" + window.location.hostname + "; path=/;";
} else {
document.cookie = `googtrans=/pt/${lang.code}; path=/;`;
document.cookie = `googtrans=/pt/${lang.code}; domain=${window.location.hostname}; path=/;`;
}
window.location.reload();
}}
className={`border-2 p-3 rounded-lg text-center cursor-pointer transition-colors ${isActive ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/30' : 'border-slate-200 dark:border-dark-border bg-white dark:bg-dark-card hover:bg-slate-50 dark:hover:bg-dark-surface'}`}
>
<div className="text-2xl mb-1">{lang.flag}</div>
<div className="text-xs font-bold text-slate-700 dark:text-slate-300">{lang.label}</div>
</div>
);
})}
</div>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2">A tradução é efetuada automaticamente e afetará toda a aplicação após recarregar a página.</p>
</div>
<div>
<h4 className="text-sm font-bold text-slate-700 mb-3">{t('apar_ncia')}</h4>
<div className="grid grid-cols-3 gap-4">
<div
onClick={() => setTheme('light')}
className={`border-2 ${theme === 'light' ? 'border-blue-500 bg-blue-50' : 'border-slate-200 bg-slate-50'} p-3 rounded-lg text-center cursor-pointer transition-colors`}
>
<div className="w-full h-12 bg-white border border-slate-200 rounded mb-2 shadow-sm"></div>
<span className={`text-xs font-bold ${theme === 'light' ? 'text-blue-600' : 'text-slate-500'}`}>{t('claro')}</span>
</div>
<div
onClick={() => setTheme('dark')}
className={`border-2 ${theme === 'dark' ? 'border-blue-500 bg-blue-50' : 'border-slate-200 bg-slate-50'} p-3 rounded-lg text-center cursor-pointer transition-colors`}
>
<div className="w-full h-12 bg-slate-800 rounded mb-2 shadow-sm border border-slate-700"></div>
<span className={`text-xs font-bold ${theme === 'dark' ? 'text-blue-600' : 'text-slate-500'}`}>{t('escuro')}</span>
</div>
<div
onClick={() => setTheme('system')}
className={`border-2 ${theme === 'system' ? 'border-blue-500 bg-blue-50' : 'border-slate-200 bg-slate-50'} p-3 rounded-lg text-center cursor-pointer transition-colors`}
>
<div className="w-full h-12 bg-gradient-to-r from-white to-slate-800 rounded mb-2 shadow-sm border border-slate-200"></div>
<span className={`text-xs font-bold ${theme === 'system' ? 'text-blue-600' : 'text-slate-500'}`}>{t('sistema')}</span>
</div>
</div>
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
};
if (!isAuthenticated) {
return <LoginView onLogin={handleLogin} />;
}
return (
<div className="flex h-screen bg-slate-50 dark:bg-dark-bg text-slate-800 dark:text-dark-text overflow-hidden font-sans transition-colors duration-300">
{/* Mobile Overlay */}
{isSidebarOpen && (
<div className="fixed inset-0 z-20 bg-black/50 lg:hidden" onClick={toggleSidebar}></div>
)}
{/* Sidebar */}
<aside className={`fixed inset-y-0 left-0 z-30 w-64 bg-white dark:bg-dark-surface border-r border-slate-200 dark:border-dark-border shadow-xl transform transition-transform duration-300 lg:translate-x-0 lg:static lg:shadow-none ${isSidebarOpen ? 'translate-x-0' : '-translate-x-full'}`}>
<div className="flex flex-col h-full">
<div className="p-6 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-600 rounded-lg text-white">
<Building2 size={24} />
</div>
<div>
<h1 className="text-xl font-bold text-slate-800 dark:text-white">{t('mycondominium')}</h1>
<p className="text-xs text-slate-400 dark:text-dark-mute">{t('portal_de_gest_o')}</p>
</div>
</div>
<button onClick={toggleSidebar} className="lg:hidden text-slate-400"><X size={24} /></button>
</div>
<div className="flex-1 px-4 py-2 space-y-1 overflow-y-auto">
<div className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2 px-4 mt-2">{t('geral')}</div>
<SidebarItem icon={Building2} label={t('dashboard')} active={activeTab === 'dashboard'} onClick={() => { setActiveTab('dashboard'); setSidebarOpen(false); }} />
{userRole === 'admin' && <SidebarItem icon={Users} label={t('cond_minos')} active={activeTab === 'residents'} onClick={() => { setActiveTab('residents'); setSidebarOpen(false); }} />}
{userRole === 'admin' && <SidebarItem icon={Wallet} label={t('finan_as')} active={activeTab === 'finance'} onClick={() => { setActiveTab('finance'); setSidebarOpen(false); }} />}
{userRole === 'admin' && <SidebarItem icon={FileText} label={t('fatura_o')} active={activeTab === 'billing'} onClick={() => { setActiveTab('billing'); setSidebarOpen(false); }} />}
{userRole === 'admin' && <SidebarItem icon={CheckCircle} label={t('pagamentos')} active={activeTab === 'approvals'} onClick={() => { setActiveTab('approvals'); setSidebarOpen(false); }} />}
{userRole === 'morador' && <SidebarItem icon={Wallet} label={t('minhas_contas')} active={activeTab === 'minhas_contas'} onClick={() => { setActiveTab('minhas_contas'); setSidebarOpen(false); }} />}
<SidebarItem icon={Wrench} label={t('manuten_o')} active={activeTab === 'maintenance'} onClick={() => { setActiveTab('maintenance'); setSidebarOpen(false); }} />
<SidebarItem icon={MessageCircle} label={t('mensagens')} active={activeTab === 'messages'} onClick={() => { setActiveTab('messages'); setSidebarOpen(false); }} />
<SidebarItem icon={Map} label={t('mapa')} active={activeTab === 'map'} onClick={() => { setActiveTab('map'); setSidebarOpen(false); }} />
<div className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2 px-4 mt-6">{t('espa_os_comuns')}</div>
<SidebarItem icon={Dumbbell} label={t('gin_sio')} active={activeTab === 'gym'} onClick={() => { setActiveTab('gym'); setSidebarOpen(false); }} />
<SidebarItem icon={PartyPopper} label={t('sal_o_de_festas')} active={activeTab === 'hall'} onClick={() => { setActiveTab('hall'); setSidebarOpen(false); }} />
<SidebarItem icon={Trophy} label={t('parque_jogos')} active={activeTab === 'park'} onClick={() => { setActiveTab('park'); setSidebarOpen(false); }} />
</div>
<div className="p-4 border-t border-slate-100 dark:border-dark-border">
<button onClick={handleLogout} className="flex items-center gap-3 px-4 py-3 text-sm font-medium text-red-600 hover:bg-red-50 w-full rounded-lg transition-colors">
<LogOut size={20} />
<span>{t('terminar_sess_o')}</span>
</button>
</div>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 flex flex-col h-full overflow-hidden relative">
{/* Header */}
<header className="h-16 bg-white dark:bg-dark-surface border-b border-slate-200 dark:border-dark-border flex items-center justify-between px-6 z-10 shadow-sm transition-colors duration-300">
<div className="flex items-center gap-4">
<button onClick={toggleSidebar} className="lg:hidden text-slate-600 dark:text-slate-300 hover:text-blue-600 p-2 -ml-2 rounded-lg hover:bg-slate-100 dark:hover:bg-dark-card">
<Menu size={24} />
</button>
<h2 className="text-xl font-bold text-slate-800 dark:text-white capitalize">{
activeTab === 'dashboard' ? 'Visão Geral' :
activeTab === 'residents' ? t('cond_minos') :
activeTab === 'finance' ? 'Gestão Financeira' :
activeTab === 'billing' ? 'Faturação e Cobranças' :
activeTab === 'maintenance' ? 'Ocorrências e Manutenção' :
activeTab === 'messages' ? 'Mensagens e Fórum' :
activeTab === 'map' ? 'Mapa do Condomínio' :
activeTab === 'all_bookings' ? 'Todas as Reservas' :
activeTab === 'gym' ? t('gin_sio') :
activeTab === 'hall' ? t('sal_o_de_festas') :
activeTab === 'park' ? t('parque_de_jogos') :
activeTab === 'profile' ? 'O Meu Perfil' : activeTab
}</h2>
</div>
<div className="flex items-center gap-4">
<div className="relative hidden md:block">
<Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<input
type="text"
placeholder="Pesquisar..."
className="pl-10 pr-4 py-2 border border-slate-200 dark:border-dark-border rounded-full text-sm focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400 w-64 bg-white dark:bg-dark-card dark:text-white dark:placeholder-slate-500"
/>
</div>
{/* Notifications */}
<div className="relative" ref={notificationRef}>
<button
className="p-2 text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-dark-card rounded-full relative transition-colors"
onClick={() => setNotificationsOpen(!isNotificationsOpen)}
>
<Bell size={20} />
{unreadNotifications > 0 && (
<span className="absolute top-1 right-1 w-2.5 h-2.5 bg-red-500 rounded-full ring-2 ring-white dark:ring-dark-surface"></span>
)}
</button>
{isNotificationsOpen && (
<div className="absolute right-0 mt-2 w-80 bg-white dark:bg-dark-surface rounded-xl shadow-xl border border-slate-100 dark:border-dark-border overflow-hidden animate-slide-up origin-top-right z-50">
<div className="p-4 border-b border-slate-100 dark:border-dark-border flex justify-between items-center">
<h3 className="font-bold text-slate-800 dark:text-white">{t('notifica_es')}</h3>
<button onClick={handleClearNotifications} className="text-xs text-blue-600 dark:text-blue-400 hover:underline">{t('limpar')}</button>
</div>
<div className="max-h-80 overflow-y-auto">
{notificationsList.length === 0 ? (
<div className="p-8 text-center text-slate-400 dark:text-dark-mute text-sm">{t('sem_novas_notifica_es')}</div>
) : (
notificationsList.map(notif => (
<div key={notif.id} className={`p-4 border-b border-slate-50 dark:border-dark-border hover:bg-slate-50 dark:hover:bg-dark-card ${!notif.read ? 'bg-blue-50/30 dark:bg-blue-900/10' : ''}`}>
<div className="flex gap-3">
<div className={`mt-1 w-2 h-2 rounded-full flex-shrink-0 ${notif.type === 'info' ? 'bg-blue-500' : notif.type === 'success' ? 'bg-green-500' : 'bg-red-500'}`}></div>
<div>
<p className="text-sm text-slate-700 dark:text-slate-200 leading-tight">{notif.message}</p>
<div className="flex justify-between items-center mt-1">
<p className="text-xs text-slate-400 dark:text-slate-500">{notif.time}</p>
{!notif.read && (
<button onClick={() => handleMarkAsRead(notif.id)} className="text-[10px] text-blue-600 dark:text-blue-400 hover:underline">
Marcar como lida
</button>
)}
</div>
</div>
</div>
</div>
))
)}
</div>
</div>
)}
</div>
<div
className="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-700/50 flex items-center justify-center text-blue-700 dark:text-blue-300 font-bold text-sm cursor-pointer hover:bg-blue-200 dark:hover:bg-blue-900/50 transition-colors"
onClick={() => setActiveTab('profile')}
title={t('meu_perfil')}
>
{(() => {
const currentUser = residents.find(r => r.id === currentUserId);
const photoUrl = userRole === 'admin' ? adminProfile?.photoUrl : currentUser?.photoUrl;
if (photoUrl) return <img src={photoUrl} alt="Perfil" className="w-full h-full rounded-full object-cover border border-slate-200 dark:border-slate-700" />;
return userRole === 'admin' ? 'AD' : 'MO';
})()}
</div>
</div>
</header>
{/* Content Body */}
<div className="flex-1 overflow-y-auto p-4 md:p-6 lg:p-8 bg-slate-50 dark:bg-dark-bg scroll-smooth transition-colors duration-300">
<div className="max-w-7xl mx-auto h-full">
{/* --- DASHBOARD --- */}
{activeTab === 'dashboard' && <DashboardView />}
{/* --- MAPA --- */}
{activeTab === 'map' && <MapView />}
{/* --- GINÁSIO --- */}
{activeTab === 'gym' && (
<BookingView
facilityType="gym"
title={t('gin_sio_privado')}
icon={Dumbbell}
description="Equipamento cardio e musculação disponível para todos os residentes."
priceInfo="Gratuito"
color="bg-gradient-to-r from-blue-500 to-blue-600"
/>
)}
{/* --- SALÃO --- */}
{activeTab === 'hall' && (
<BookingView
facilityType="hall"
title={t('sal_o_de_festas')}
icon={PartyPopper}
description="Espaço amplo para eventos, aniversários e reuniões de condomínio."
priceInfo="50€ / Evento"
color="bg-gradient-to-r from-purple-500 to-purple-600"
/>
)}
{/* --- PARQUE --- */}
{activeTab === 'park' && (
<BookingView
facilityType="park"
title={t('parque_de_jogos')}
icon={Trophy}
description="Campo polidesportivo exterior (Futebol, Basquetebol e Ténis)."
priceInfo="10€ / Hora"
color="bg-gradient-to-r from-green-500 to-green-600"
/>
)}
{/* --- ALL BOOKINGS --- */}
{activeTab === 'all_bookings' && (
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border overflow-hidden animate-fade-in flex flex-col h-full transition-colors">
<div className="p-6 border-b border-slate-100 dark:border-dark-border flex justify-between items-center">
<div>
<h3 className="font-bold text-lg text-slate-800 dark:text-white">{t('hist_rico_de_reservas')}</h3>
<p className="text-sm text-slate-500 dark:text-dark-mute">{t('lista_completa_de_agendamentos_em_todos_os_espa_os_de_lazer')}</p>
</div>
<button onClick={() => setActiveTab('map')} className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 flex items-center justify-center gap-2 shadow-sm transition-colors">
<Map size={18} /> Nova Reserva
</button>
</div>
<div className="overflow-auto flex-1 p-4 space-y-3">
{bookings.map(booking => (
<div key={booking.id} className="flex items-center justify-between p-4 border border-slate-100 dark:border-dark-border rounded-lg hover:bg-slate-50 dark:hover:bg-dark-card transition-colors shadow-sm">
<div className="flex items-center gap-4">
<div className={`p-3 rounded-lg ${booking.facility === 'gym' ? 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400' : booking.facility === 'hall' ? 'bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400' : 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400'}`}>
{booking.facility === 'gym' ? <Dumbbell size={24} /> : booking.facility === 'hall' ? <PartyPopper size={24} /> : <Trophy size={24} />}
</div>
<div>
<p className="font-bold text-base text-slate-800 dark:text-slate-200">{booking.facilityName}</p>
<p className="text-sm text-slate-600 dark:text-slate-400">{booking.date} {booking.time}</p>
</div>
</div>
<div className="text-right">
<p className="text-base font-medium text-slate-700 dark:text-slate-300">{booking.resident}</p>
<div className="mt-1">
<Badge status={booking.status} />
</div>
</div>
</div>
))}
{bookings.length === 0 && (
<div className="text-center py-12">
<Calendar size={48} className="mx-auto text-slate-300 dark:text-slate-600 mb-4" />
<h3 className="text-lg font-bold text-slate-700 dark:text-slate-300">{t('sem_reservas')}</h3>
<p className="text-slate-500 dark:text-slate-400 mt-2">Ainda não existem agendamentos no condomínio.</p>
</div>
)}
</div>
</div>
)}
{/* --- APPROVALS --- */}
{activeTab === 'approvals' && userRole === 'admin' && (
<div className="p-6">
<div className="mb-6">
<h2 className="text-2xl font-bold text-slate-800 dark:text-white">{t('pagamentos_conclu_dos')}</h2>
<p className="text-slate-500 dark:text-dark-mute">Consulte o histórico de todos os pagamentos concluídos pelos condóminos.</p>
</div>
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border overflow-hidden">
<div className="overflow-x-auto">
<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">{t('morador')}</th>
<th className="p-4">{t('fatura')}</th>
<th className="p-4">{t('estado')}</th>
<th className="p-4 text-right">{t('valor')}</th>
</tr>
</thead>
<tbody>
{faturas.filter(f => isPaidStatus(f.status)).map(fatura => (
<tr key={fatura.id} className="border-b border-slate-50 dark:border-dark-border hover:bg-slate-50/50 dark:hover:bg-dark-bg/50">
<td className="p-4">
<p className="font-semibold text-slate-700 dark:text-slate-200">{fatura.nomeMorador}</p>
<p className="text-xs text-slate-400">Fração: {fatura.fracao}</p>
</td>
<td className="p-4 text-slate-600 dark:text-slate-400">
<p className="text-sm">{fatura.categoria}</p>
<p className="text-xs">Venceu a: {fatura.dataVencimento}</p>
</td>
<td className="p-4 text-slate-600 dark:text-slate-400">
<div className="inline-flex items-center gap-1.5 px-2 py-1 rounded bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 text-xs font-medium">
<CheckCircle size={14} /> Pago
</div>
</td>
<td className="p-4 font-bold text-green-600 dark:text-green-400 text-right">{Number(fatura.valor).toFixed(2)}</td>
</tr>
))}
{faturas.filter(f => isPaidStatus(f.status)).length === 0 && (
<tr><td colSpan="4" className="p-8 text-center text-slate-500">Nenhum pagamento concluído encontrado.</td></tr>
)}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* --- RESIDENTS --- */}
{activeTab === 'residents' && (
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border overflow-hidden animate-fade-in flex flex-col h-full transition-colors">
<div className="p-6 border-b border-slate-100 dark:border-dark-border flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h3 className="font-bold text-lg text-slate-800 dark:text-white">{t('gest_o_de_cond_minos')}</h3>
<p className="text-sm text-slate-500 dark:text-dark-mute">Total: {residents.length} frações registadas</p>
</div>
<div className="flex flex-col sm:flex-row gap-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={18} />
<input
type="text"
placeholder="Procurar fração ou nome..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 pr-4 py-2 border border-slate-200 dark:border-dark-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-full sm:w-64 bg-white dark:bg-dark-card dark:text-white dark:placeholder-slate-500 transition-colors"
/>
</div>
<button
onClick={() => handleOpenModal('resident')}
className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 flex items-center justify-center gap-2 transition-colors shadow-sm"
>
<Plus size={18} /> <span className="hidden sm:inline">{t('adicionar')}</span>
</button>
</div>
</div>
<div className="overflow-x-auto flex-1">
<table className="w-full text-sm text-left">
<thead className="bg-slate-50 dark:bg-dark-bg text-slate-500 dark:text-slate-400 font-medium border-b border-slate-100 dark:border-dark-border sticky top-0">
<tr>
<th className="px-6 py-4">{t('fra_o')}</th>
<th className="px-6 py-4">{t('propriet_rio')}</th>
<th className="px-6 py-4">{t('contacto')}</th>
<th className="px-6 py-4">{t('estado_quotas')}</th>
<th className="px-6 py-4 text-center">{t('acesso')}</th>
<th className="px-6 py-4 text-right">{t('em_d_vida')}</th>
<th className="px-6 py-4 text-center">{t('a_es')}</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 dark:divide-dark-border">
{filteredResidents.map((resident) => (
<tr key={resident.id} className="hover:bg-slate-50 dark:hover:bg-dark-bg transition-colors group">
<td className="px-6 py-4 font-bold text-slate-700 dark:text-slate-200">{resident.unit}</td>
<td className="px-6 py-4">
<div className="flex flex-col">
<span className="text-slate-900 dark:text-white font-medium">{resident.name}</span>
<span className="text-xs text-slate-500 dark:text-dark-mute">{resident.email}</span>
</div>
</td>
<td className="px-6 py-4 text-slate-600 dark:text-dark-mute">{resident.contact}</td>
<td className="px-6 py-4"><Badge status={resident.status} /></td>
<td className="px-6 py-4 text-center">
<button
onClick={() => handleToggleRole(resident.id)}
className={`px-3 py-1 rounded-full text-xs font-bold transition-colors ${resident.role === 'admin' ? 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400 dark:hover:bg-purple-900/50' : 'bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700'}`}
title={t('mudar_permiss_es')}
>
{resident.role === 'admin' ? 'Admin' : 'Morador'}
</button>
</td>
<td className={`px-6 py-4 text-right font-medium ${resident.pending > 0 ? 'text-red-600 dark:text-red-400' : 'text-slate-600 dark:text-slate-400'}`}>
{Number(resident.pending).toFixed(2)}
</td>
<td className="px-6 py-4 text-center">
<div className="flex items-center justify-center gap-2 opacity-100 sm:opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => handleOpenModal('resident', resident)}
className="p-1.5 text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/30 rounded-lg transition-colors" title={t('editar')}
>
<Edit2 size={16} />
</button>
<button
onClick={() => handleDeleteResident(resident.id)}
className="p-1.5 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors" title={t('eliminar')}
>
<Trash2 size={16} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* --- BILLING / COBRANÇAS --- */}
{activeTab === 'billing' && userRole === 'admin' && (
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border overflow-hidden animate-fade-in flex flex-col h-full transition-colors">
<div className="p-6 border-b border-slate-100 dark:border-dark-border flex flex-col md:flex-row justify-between items-center gap-4">
<div>
<h3 className="font-bold text-lg text-slate-800 dark:text-white">{t('avisos_de_cobran_a')}</h3>
<p className="text-sm text-slate-500 dark:text-dark-mute">{t('emita_faturas_ou_avise_cond_minos_individualmente')}</p>
</div>
<button onClick={() => handleOpenModal('emitir_fatura')} className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 flex items-center justify-center gap-2 shadow-sm">
<FileText size={18} /> Emitir Fatura
</button>
</div>
<div className="overflow-x-auto flex-1">
<table className="w-full text-sm text-left">
<thead className="bg-slate-50 dark:bg-dark-bg text-slate-500 dark:text-slate-400 font-medium border-b border-slate-100 dark:border-dark-border">
<tr>
<th className="px-6 py-4">{t('fra_o')}</th>
<th className="px-6 py-4">{t('cond_mino')}</th>
<th className="px-6 py-4">{t('quotas_em_atraso')}</th>
<th className="px-6 py-4 text-center">{t('a_es')}</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 dark:divide-dark-border">
{residents.map((resident) => (
<tr key={resident.id} className="hover:bg-slate-50 dark:hover:bg-dark-bg transition-colors">
<td className="px-6 py-4 font-bold text-slate-700 dark:text-slate-200">{resident.unit}</td>
<td className="px-6 py-4 text-slate-800 dark:text-white">{resident.name}</td>
<td className={`px-6 py-4 font-medium ${resident.pending > 0 ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-500'}`}>
{resident.pending > 0 ? `${Number(resident.pending).toFixed(2)}` : 'Regularizado'}
</td>
<td className="px-6 py-4 text-center">
<button
onClick={() => {
showNotification(`Aviso de cobrança enviado para ${resident.email}`, 'success');
sendSystemNotification(`Aviso de cobrança enviado a ${resident.name} para a fração ${resident.unit}.`, 'info', resident.id);
}}
className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-colors shadow-sm cursor-pointer ${resident.pending > 0 ? 'bg-orange-600 text-white hover:bg-orange-700' : 'bg-slate-200 text-slate-400 cursor-not-allowed dark:bg-slate-800 dark:text-slate-600'}`}
disabled={resident.pending <= 0}
>
Notificar
</button>
<button
onClick={() => handleGenerateInvoice(resident)}
className="ml-2 px-3 py-1.5 rounded-lg text-xs font-bold transition-colors shadow-sm bg-slate-800 text-white hover:bg-slate-900 dark:bg-slate-700 dark:hover:bg-slate-600"
>
Faturar na Hora
</button>
<button
onClick={() => {
showNotification(`Fatura enviada com sucesso para ${resident.email}`, 'success');
sendSystemNotification(`Fatura enviada a ${resident.name} (${resident.unit}) por email.`, 'info', resident.id);
}}
className="ml-2 px-3 py-1.5 rounded-lg text-xs font-bold transition-colors shadow-sm bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700"
>
Enviar por Email
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* --- MINHAS CONTAS (Morador) --- */}
{activeTab === 'minhas_contas' && userRole === 'morador' && (
<div className="space-y-6 animate-fade-in h-full flex flex-col">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white dark:bg-dark-surface p-6 rounded-xl shadow-sm border border-slate-100 dark:border-dark-border border-l-4 border-l-orange-500 relative overflow-hidden transition-colors">
<div className="relative z-10">
<h4 className="text-slate-500 dark:text-dark-mute text-xs font-bold uppercase tracking-wider">{t('total_pendente')}</h4>
<p className="text-2xl font-bold text-slate-800 dark:text-white mt-1">
{faturas.filter(f => f.moradorId === currentUserId && isPendingStatus(f.status)).reduce((acc, f) => acc + Number(f.valor), 0).toFixed(2)}
</p>
</div>
<AlertCircle className="absolute right-4 bottom-4 text-orange-100 dark:text-orange-900/20" size={64} />
</div>
<div className="bg-white dark:bg-dark-surface p-6 rounded-xl shadow-sm border border-slate-100 dark:border-dark-border border-l-4 border-l-green-500 relative overflow-hidden transition-colors">
<div className="relative z-10">
<h4 className="text-slate-500 dark:text-dark-mute text-xs font-bold uppercase tracking-wider">{t('total_pago')}</h4>
<p className="text-2xl font-bold text-slate-800 dark:text-white mt-1">
{faturas.filter(f => f.moradorId === currentUserId && isPaidStatus(f.status)).reduce((acc, f) => acc + Number(f.valor), 0).toFixed(2)}
</p>
</div>
<CheckCircle className="absolute right-4 bottom-4 text-green-100 dark:text-green-900/20" size={64} />
</div>
</div>
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border overflow-hidden flex-1 flex flex-col transition-colors">
<div className="p-6 border-b border-slate-100 dark:border-dark-border flex flex-col md:flex-row justify-between items-center gap-4">
<div>
<h3 className="font-bold text-lg text-slate-800 dark:text-white">{t('minhas_faturas')}</h3>
<p className="text-sm text-slate-500 dark:text-dark-mute">{t('consulte_as_suas_despesas_e_faturas_emitidas')}</p>
</div>
</div>
<div className="overflow-x-auto flex-1">
<table className="w-full text-sm text-left">
<thead className="bg-slate-50 dark:bg-dark-bg text-slate-500 dark:text-slate-400 font-medium border-b border-slate-100 dark:border-dark-border">
<tr>
<th className="px-6 py-4">{t('data_emiss_o')}</th>
<th className="px-6 py-4">{t('categoria')}</th>
<th className="px-6 py-4">{t('vencimento')}</th>
<th className="px-6 py-4 text-right">{t('valor')}</th>
<th className="px-6 py-4 text-center">{t('estado')}</th>
<th className="px-6 py-4 text-center">{t('a_es')}</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 dark:divide-dark-border">
{faturas.filter(f => f.moradorId === currentUserId).length === 0 ? (
<tr>
<td colSpan="6" className="px-6 py-8 text-center text-slate-500 dark:text-slate-400">
Nenhuma fatura encontrada.
</td>
</tr>
) : (
faturas.filter(f => f.moradorId === currentUserId).map((fatura) => (
<tr key={fatura.id} className="hover:bg-slate-50 dark:hover:bg-dark-bg transition-colors">
<td className="px-6 py-4 text-slate-600 dark:text-slate-400">{fatura.dataEmissao}</td>
<td className="px-6 py-4 font-medium text-slate-800 dark:text-white">{fatura.categoria}</td>
<td className="px-6 py-4 text-slate-600 dark:text-slate-400">{fatura.dataVencimento}</td>
<td className="px-6 py-4 text-right font-medium text-slate-800 dark:text-white">{Number(fatura.valor).toFixed(2)}</td>
<td className="px-6 py-4 text-center"><Badge status={fatura.status} /></td>
<td className="px-6 py-4 text-center">
{isPendingStatus(fatura.status) ? (
<button
onClick={() => handlePayFatura(fatura)}
className="bg-blue-600 text-white px-4 py-1.5 rounded-lg text-xs font-bold hover:bg-blue-700 transition-colors shadow-sm"
>
Pagar
</button>
) : (
<span className="text-slate-400 text-xs font-bold flex items-center justify-center gap-1">
<CheckCircle size={14} /> Pago
</span>
)}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* --- FINANCES --- */}
{/* --- FINANCES --- */}
{activeTab === 'finance' && (
<div className="space-y-6 animate-fade-in h-full flex flex-col">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-white dark:bg-dark-surface p-6 rounded-xl shadow-sm border border-slate-100 dark:border-dark-border border-l-4 border-l-green-500 relative overflow-hidden transition-colors">
<div className="relative z-10">
<h4 className="text-slate-500 dark:text-dark-mute text-xs font-bold uppercase tracking-wider">Receitas (Global)</h4>
<p className="text-2xl font-bold text-slate-800 dark:text-white mt-1">{totalIncome.toFixed(2)}</p>
</div>
<TrendingUp className="absolute right-4 bottom-4 text-green-100 dark:text-green-900/20" size={64} />
</div>
<div className="bg-white dark:bg-dark-surface p-6 rounded-xl shadow-sm border border-slate-100 dark:border-dark-border border-l-4 border-l-red-500 relative overflow-hidden transition-colors">
<div className="relative z-10">
<h4 className="text-slate-500 dark:text-dark-mute text-xs font-bold uppercase tracking-wider">Despesas (Global)</h4>
<p className="text-2xl font-bold text-slate-800 dark:text-white mt-1">{totalExpense.toFixed(2)}</p>
</div>
<TrendingDown className="absolute right-4 bottom-4 text-red-100 dark:text-red-900/20" size={64} />
</div>
<div className="bg-white dark:bg-dark-surface p-6 rounded-xl shadow-sm border border-slate-100 dark:border-dark-border border-l-4 border-l-blue-500 relative overflow-hidden transition-colors">
<div className="relative z-10">
<h4 className="text-slate-500 dark:text-dark-mute text-xs font-bold uppercase tracking-wider">{t('balan_o_l_quido')}</h4>
<p className={`text-2xl font-bold mt-1 ${balance >= 0 ? 'text-blue-600 dark:text-blue-400' : 'text-red-600 dark:text-red-400'}`}>
{balance > 0 ? '+' : ''}{balance.toFixed(2)}
</p>
</div>
<Wallet className="absolute right-4 bottom-4 text-blue-100 dark:text-blue-900/20" size={64} />
</div>
</div>
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border overflow-hidden flex-1 flex flex-col transition-colors">
<div className="p-6 border-b border-slate-100 dark:border-dark-border flex justify-between items-center">
<div className="flex items-center gap-2">
<h3 className="font-bold text-lg text-slate-800 dark:text-white">{t('di_rio_financeiro')}</h3>
<span className="text-xs bg-slate-100 dark:bg-dark-bg text-slate-600 dark:text-slate-400 px-2 py-1 rounded-full">{finances.length} movimentos</span>
</div>
<div className="flex items-center gap-3">
{finances.length === 0 && (
<button
onClick={async () => {
try {
for (const item of INITIAL_FINANCES) {
await set(push(ref(db, 'financas')), item);
}
showNotification("Dados de exemplo restaurados com sucesso!", "success");
} catch (error) {
console.error("Erro ao restaurar:", error);
showNotification("Erro ao restaurar.", "error");
}
}}
className="bg-orange-500 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-orange-600 transition-colors shadow-sm flex items-center gap-2"
>
<Wrench size={16} /> Restaurar Base de Dados
</button>
)}
<button
onClick={() => handleOpenModal('finance')}
className="bg-slate-900 dark:bg-slate-700 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-slate-800 dark:hover:bg-slate-600 flex items-center gap-2 shadow-lg hover:shadow-xl transition-all"
>
<Plus size={18} /> Novo Registo
</button>
</div>
</div>
<div className="overflow-auto flex-1">
<table className="w-full text-sm text-left">
<thead className="bg-slate-50 dark:bg-dark-bg text-slate-500 dark:text-slate-400 font-medium border-b border-slate-100 dark:border-dark-border sticky top-0 z-10">
<tr>
<th className="px-6 py-4">{t('data')}</th>
<th className="px-6 py-4">{t('categoria')}</th>
<th className="px-6 py-4">{t('descri_o')}</th>
<th className="px-6 py-4">{t('tipo')}</th>
<th className="px-6 py-4 text-right">{t('valor')}</th>
<th className="px-6 py-4 text-center">{t('recibo')}</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 dark:divide-dark-border">
{finances.map((item) => (
<tr key={item.id} className="hover:bg-slate-50 dark:hover:bg-dark-bg transition-colors">
<td className="px-6 py-4 text-slate-600 dark:text-slate-400 font-mono">{item.date}</td>
<td className="px-6 py-4 font-bold text-slate-700 dark:text-slate-200">{item.category}</td>
<td className="px-6 py-4 text-slate-500 dark:text-dark-mute">{item.desc}</td>
<td className="px-6 py-4">
<Badge status={item.type === 'income' ? t('receita') : 'Despesa'} />
</td>
<td className={`px-6 py-4 text-right font-bold font-mono ${item.type === 'income' ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}>
{item.type === 'income' ? '+' : '-'}{Number(item.amount).toFixed(2)}
</td>
<td className="px-6 py-4 text-center">
<button onClick={() => {
showNotification(`Recibo de ${item.category} descarregado.`, 'success');
sendSystemNotification(`Recibo de ${item.category} descarregado.`, 'info', currentUserId);
}} className="text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors" title={t('descarregar_recibo')}>
<FileText size={16} />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* --- MESSAGES --- */}
{activeTab === 'messages' && (
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border overflow-hidden animate-fade-in flex flex-col h-full transition-colors md:flex-row min-h-[500px]">
{/* Contact List */}
<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">
<h3 className="font-bold text-slate-800 dark:text-white">{t('conversas')}</h3>
<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={t('criar_grupo')}>
<Plus size={18} />
</button>
</div>
<div className="p-3 border-b border-slate-100 dark:border-dark-border">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={16} />
<input type="text" placeholder="Procurar mensagens..." className="w-full pl-9 pr-3 py-2 bg-slate-100 dark:bg-dark-card border-none rounded-lg text-sm focus:ring-2 focus:ring-blue-500 dark:text-white" />
</div>
</div>
<div className="flex-1 overflow-y-auto">
<div
onClick={() => setActiveChat({ type: 'global', id: 'global', name: t('f_rum_do_condom_nio') })}
className={`p-3 border-b-2 cursor-pointer transition-colors ${activeChat.type === 'global' ? 'border-blue-100 dark:border-blue-900/30 bg-blue-50/50 dark:bg-blue-900/10 hover:bg-blue-50/80 dark:hover:bg-blue-900/20' : 'border-transparent hover:bg-slate-50 dark:hover:bg-dark-card'}`}
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center text-blue-600 dark:text-blue-400 font-bold shrink-0">
<Users size={20} />
</div>
<div className="flex-1 min-w-0">
<div className="flex justify-between items-baseline mb-0.5">
<h4 className="text-sm font-bold text-slate-800 dark:text-slate-200 truncate">{t('f_rum_do_condom_nio')}</h4>
<span className="text-xs text-blue-600 dark:text-blue-400 font-medium whitespace-nowrap">{t('geral')}</span>
</div>
<p className="text-xs text-slate-500 dark:text-slate-400 truncate">{t('grupo_partilhado')}</p>
</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">{t('grupo')}</p>
</div>
</div>
</div>
))}
{residents.filter(r => r.id !== currentUserId).map(res => (
<div
key={res.id}
onClick={() => setActiveChat({ type: 'private', id: res.id, name: res.name })}
className={`p-3 border-b border-slate-50 dark:border-dark-border/50 cursor-pointer transition-colors ${activeChat.id === res.id ? 'bg-blue-50/50 dark:bg-blue-900/10' : 'hover:bg-slate-50 dark:hover:bg-dark-card'}`}
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center text-slate-600 dark:text-slate-300 font-bold shrink-0 text-sm">
{res.name.substring(0, 2).toUpperCase()}
</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">{res.name} {res.unit && `(${res.unit})`}</h4>
{activeChat.id === res.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">{t('morador')}</p>
</div>
</div>
</div>
))}
</div>
</div>
{/* Chat Area */}
<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="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' : 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' || activeChat.type === 'group' ? <Users size={20} /> : activeChat.name.substring(0, 2).toUpperCase()}
</div>
<div>
<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' : activeChat.type === 'group' ? 'Grupo Privado' : 'Privado'}</p>
</div>
</div>
<button className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200"><Info size={20}/></button>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-4 flex flex-col">
<div className="text-center my-4"><span className="text-xs bg-slate-200/50 dark:bg-slate-800 text-slate-500 px-3 py-1 rounded-full">{t('mensagens')}</span></div>
{messages.map((msg) => {
const isMe = msg.senderId === currentUserId;
const timeString = new Date(msg.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
return (
<div key={msg.id} className={`flex ${isMe ? 'justify-end' : 'justify-start'}`}>
<div className={`${isMe ? 'bg-blue-600 text-white rounded-2xl rounded-tr-sm' : 'bg-white dark:bg-dark-card border border-slate-100 dark:border-slate-800 text-slate-700 dark:text-slate-200 rounded-2xl rounded-tl-sm'} p-3 max-w-[80%] shadow-sm`}>
{!isMe && (
<p className={`text-xs ${msg.role === 'admin' ? 'text-blue-600 dark:text-blue-400' : 'text-orange-600 dark:text-orange-400'} font-bold mb-1`}>
{msg.senderName}
</p>
)}
<p className="text-sm">{msg.text}</p>
<span className={`text-[10px] ${isMe ? 'text-blue-200' : 'text-slate-400'} block text-right mt-1`}>{timeString}</span>
</div>
</div>
);
})}
</div>
<div className="p-4 bg-white dark:bg-dark-surface border-t border-slate-100 dark:border-dark-border">
<form onSubmit={async (e) => {
e.preventDefault();
if (!newMessageText.trim()) return;
try {
const path = activeChat.type === 'global'
? 'mural_mensagens'
: activeChat.type === 'group'
? `mensagens_grupo/${activeChat.id}`
: `mensagens_privadas/${[currentUserId, activeChat.id].sort().join('_')}`;
const newMsgRef = push(ref(db, path));
const newMessage = {
text: newMessageText,
senderId: currentUserId,
senderName: currentUserName,
role: userRole,
timestamp: Date.now()
};
await set(newMsgRef, newMessage);
setMessages(prev => [...prev, { id: newMsgRef.key, ...newMessage }]);
setNewMessageText('');
} catch (error) {
console.error("Erro ao enviar mensagem:", error);
showNotification("Erro ao enviar mensagem.", "error");
}
}} className="flex gap-2">
<button type="button" className="p-2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors hidden sm:block">
<Paperclip size={20} />
</button>
<input
type="text"
value={newMessageText}
onChange={(e) => setNewMessageText(e.target.value)}
placeholder={t('escreva_a_sua_mensagem')}
className="flex-1 bg-slate-50 dark:bg-dark-bg border border-slate-200 dark:border-dark-border rounded-full px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:text-white"
/>
<button type="submit" disabled={!newMessageText.trim()} className="p-2 bg-blue-600 text-white rounded-full hover:bg-blue-700 disabled:bg-blue-400 transition-colors shadow-sm flex items-center justify-center shrink-0 w-10 h-10">
<Send size={18} />
</button>
</form>
</div>
</div>
</div>
)}
{/* --- MAINTENANCE --- */}
{activeTab === 'maintenance' && <MaintenanceView />}
{/* --- PROFILE --- */}
{activeTab === 'profile' && <ProfileView theme={theme} setTheme={setTheme} />}
</div>
</div>
{/* --- Toast Notification --- */}
{notification && (
<div className={`fixed bottom-6 right-6 px-6 py-4 rounded-xl shadow-2xl flex items-center gap-3 animate-slide-up z-50 text-white ${notification.type === 'error' ? 'bg-red-600' : notification.type === 'warning' ? 'bg-orange-600' : 'bg-slate-800'}`}>
{notification.type === 'success' ? <CheckCircle size={20} className="text-green-400" /> : <AlertCircle size={20} />}
<span className="font-medium">{notification.message}</span>
</div>
)}
{/* --- Modals --- */}
{/* Resident Modal */}
<Modal isOpen={activeModal === 'resident'} onClose={handleCloseModal} title={editingItem ? "Editar Condómino" : "Novo Condómino"}>
<form onSubmit={handleSaveResident}>
<InputGroup label={t('fra_o')} name="unit" value={formData.unit || ''} onChange={handleInputChange} placeholder={t('ex_1_esq')} required />
<InputGroup label={t('nome_completo')} name="name" value={formData.name || ''} onChange={handleInputChange} placeholder={t('nome_do_propriet_rio')} required />
<InputGroup label={t('email')} type="email" name="email" value={formData.email || ''} onChange={handleInputChange} placeholder="email@exemplo.com" />
<div className="grid grid-cols-2 gap-4">
<InputGroup label={t('telem_vel')} name="contact" value={formData.contact || ''} onChange={handleInputChange} placeholder="912 345 678" required />
<InputGroup label={t('palavra_passe')} type="text" name="password" value={formData.password || ''} onChange={handleInputChange} placeholder={editingItem ? "Deixar em branco para manter" : "1234"} required={!editingItem} />
</div>
<div className="grid grid-cols-2 gap-4">
<InputGroup label={t('estado')} name="status" value={formData.status || 'Pago'} onChange={handleInputChange} options={[{ value: 'Pago', label: 'Pago' }, { value: 'Pendente', label: 'Pendente' }, { value: 'Atrasado', label: 'Atrasado' }]} />
<InputGroup label={t('valor_pendente')} type="number" name="pending" value={formData.pending || 0} onChange={handleInputChange} />
</div>
<button type="submit" className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded-lg mt-4 flex justify-center gap-2">
<Save size={20} /> Guardar Condómino
</button>
</form>
</Modal>
{/* Emitir Fatura Modal */}
<Modal isOpen={activeModal === 'emitir_fatura'} onClose={handleCloseModal} title={t('emitir_nova_fatura')}>
<form onSubmit={handleSaveFatura}>
<div className="mb-4">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">{t('cond_mino')}</label>
<select
name="moradorId"
value={formData.moradorId || ''}
onChange={handleInputChange}
className="w-full px-4 py-2 border border-slate-300 dark:border-dark-border rounded-lg bg-white dark:bg-dark-card text-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500"
required
>
<option value="">Selecione um morador...</option>
{residents.map(r => (
<option key={r.id} value={r.id}>{r.unit} - {r.name}</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<InputGroup label={t('categoria')} name="categoria" value={formData.categoria || 'Quotas'} onChange={handleInputChange} options={[{value: 'Quotas', label: 'Quotas'}, {value: 'Luz', label: 'Luz'}, {value: 'Água', label: 'Água'}, {value: 'Aluguer', label: 'Aluguer'}, {value: 'Manutenção', label: 'Manutenção'}, {value: 'Outros', label: 'Outros'}]} />
<InputGroup label={t('valor')} type="number" name="valor" value={formData.valor || ''} onChange={handleInputChange} placeholder="0.00" required />
</div>
<InputGroup label={t('data_vencimento')} type="date" name="dataVencimento" value={formData.dataVencimento || ''} onChange={handleInputChange} required />
<button type="submit" className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded-lg mt-4 flex justify-center gap-2 transition-colors">
<FileText size={20} /> Emitir Fatura
</button>
</form>
</Modal>
{/* Finance Modal */}
<Modal isOpen={activeModal === 'finance'} onClose={handleCloseModal} title={t('registar_movimento_financeiro')}>
<form onSubmit={handleSaveFinance}>
<div className="grid grid-cols-2 gap-4">
<InputGroup label={t('tipo')} name="type" value={formData.type || 'expense'} onChange={handleInputChange} options={[{ value: 'income', label: 'Receita (+)' }, { value: 'expense', label: 'Despesa (-)' }]} />
<InputGroup label={t('data')} type="date" name="date" value={formData.date || ''} onChange={handleInputChange} required />
</div>
<InputGroup label={t('categoria')} name="category" value={formData.category || ''} onChange={handleInputChange} placeholder={t('ex_limpeza_elevadores_quotas')} required />
<InputGroup label={t('valor')} type="number" name="amount" value={formData.amount || ''} onChange={handleInputChange} placeholder="0.00" required />
<InputGroup label={t('descri_o')} name="desc" value={formData.desc || ''} onChange={handleInputChange} placeholder={t('detalhes_do_movimento')} />
<button type="submit" className="w-full bg-slate-800 hover:bg-slate-900 dark:bg-slate-700 dark:hover:bg-slate-600 text-white font-bold py-3 rounded-lg mt-4 flex justify-center gap-2 transition-colors">
<Save size={20} /> Registar Movimento
</button>
</form>
</Modal>
{/* Issue Modal */}
<Modal isOpen={activeModal === 'issue'} onClose={handleCloseModal} title={t('reportar_ocorr_ncia')}>
<form onSubmit={handleSaveIssue}>
<InputGroup label={t('t_tulo_do_problema')} name="title" value={formData.title || ''} onChange={handleInputChange} placeholder={t('ex_l_mpada_fundida')} required />
<InputGroup label={t('localiza_o')} name="location" value={formData.location || ''} onChange={handleInputChange} placeholder={t('ex_hall_de_entrada')} required />
<div className="grid grid-cols-2 gap-4">
<InputGroup label={t('prioridade')} name="priority" value={formData.priority || 'Média'} onChange={handleInputChange} options={[{ value: 'Baixa', label: 'Baixa' }, { value: 'Média', label: 'Média' }, { value: 'Alta', label: 'Alta' }]} />
<InputGroup label={t('data')} type="date" name="date" value={formData.date || ''} onChange={handleInputChange} />
</div>
<button type="submit" className="w-full bg-orange-600 hover:bg-orange-700 text-white font-bold py-3 rounded-lg mt-4 flex justify-center gap-2">
<Save size={20} /> Reportar
</button>
</form>
</Modal>
{/* Booking Modal */}
<Modal isOpen={activeModal === 'booking'} onClose={handleCloseModal} title={t('nova_reserva')}>
<form onSubmit={handleSaveBooking}>
<InputGroup label={t('espa_o')} name="facility" value={formData.facility || 'gym'} onChange={handleInputChange} options={[{ value: 'gym', label: 'Ginásio' }, { value: 'hall', label: t('sal_o_de_festas') }, { value: 'park', label: t('parque_de_jogos') }]} />
<div className="grid grid-cols-2 gap-4">
<InputGroup label={t('data')} type="date" name="date" value={formData.date || ''} onChange={handleInputChange} required />
<InputGroup label={t('hor_rio')} name="time" value={formData.time || ''} onChange={handleInputChange} placeholder={t('ex_14_00_16_00')} required />
</div>
<InputGroup label={t('reservado_para_cond_mino')} name="resident" value={formData.resident || ''} onChange={handleInputChange} placeholder={t('nome_do_residente')} required disabled={userRole !== 'admin'} />
<div className="bg-slate-50 dark:bg-dark-card p-4 rounded-lg border border-slate-200 dark:border-dark-border mt-2 mb-4 flex justify-between items-center transition-colors">
<span className="text-sm font-medium text-slate-600 dark:text-slate-300">Custo Estimado:</span>
<span className="text-xl font-bold text-slate-800 dark:text-white">{formData.cost || 0}</span>
</div>
<button type="submit" className="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 rounded-lg flex justify-center gap-2">
<CheckCircle size={20} /> Confirmar Reserva
</button>
</form>
</Modal>
<Modal isOpen={isCreateGroupModalOpen} onClose={() => setIsCreateGroupModalOpen(false)} title={t('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={t('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">{t('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>
<Modal isOpen={confirmDialog.isOpen} onClose={() => setConfirmDialog(prev => ({ ...prev, isOpen: false }))} title={t('confirma_o')}>
<div className="p-2">
<p className="text-slate-700 dark:text-slate-300 text-base mb-6 text-center font-medium">{confirmDialog.message}</p>
<div className="flex gap-3 justify-center">
<button
className="px-6 py-2 bg-slate-200 dark:bg-slate-700 hover:bg-slate-300 dark:hover:bg-slate-600 text-slate-800 dark:text-white rounded-lg transition-colors font-medium"
onClick={() => setConfirmDialog(prev => ({ ...prev, isOpen: false }))}
>
Cancelar
</button>
<button
className="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors font-medium shadow-sm"
onClick={() => {
setConfirmDialog(prev => ({ ...prev, isOpen: false }));
if (confirmDialog.onConfirm) confirmDialog.onConfirm();
}}
>
Confirmar
</button>
</div>
</div>
</Modal>
</main>
</div>
);
}
const root = createRoot(document.getElementById('root'));
root.render(
<ErrorBoundary>
<LanguageProvider>
<App />
</LanguageProvider>
</ErrorBoundary>
);
</script>
<!-- Firebase configs moved to top in React Module -->
</body>
</html>