From 0fe52b062580722aedaf1e72d0873dfcd52ae1d0 Mon Sep 17 00:00:00 2001 From: Ricardo <230414@epvc.pt> Date: Tue, 28 Apr 2026 17:17:12 +0100 Subject: [PATCH] =?UTF-8?q?notifica=C3=A7oes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 389 +++++++++++++++++++++++++++++++++++++++++++++++++--- test_nif.js | 15 ++ 2 files changed, 388 insertions(+), 16 deletions(-) create mode 100644 test_nif.js diff --git a/index.html b/index.html index 532b751..5db40ff 100644 --- a/index.html +++ b/index.html @@ -139,6 +139,60 @@ { id: 3, message: 'Manutenção urgente reportada', time: 'Há 5 horas', type: 'warning', read: false }, ]; + // --- VALIDAÇÕES OFICIAIS --- + function validarNIF(nif) { + nif = nif.replace(/\s+/g, ''); + if (!/^\d{9}$/.test(nif)) return false; + if (nif.charAt(0) === '0') return false; + let soma = 0; + for (let i = 0; i < 8; i++) { + soma += parseInt(nif.charAt(i), 10) * (9 - i); + } + const resto = soma % 11; + const digitoControlo = (resto === 0 || resto === 1) ? 0 : (11 - resto); + return digitoControlo === parseInt(nif.charAt(8), 10); + } + + function validarDocumento(doc) { + let docStr = doc.replace(/[\s-]/g, '').toUpperCase(); + + // Muitos utilizadores inserem apenas os 8 dígitos do NIC, o que não tem check-digit na própria string + if (/^\d{8}$/.test(docStr)) { + return true; + } + + if (/^\d{9}$/.test(docStr)) { + let checkDigitValue = parseInt(docStr.charAt(docStr.length - 1), 10); + let soma = 0; + for (let i = 0; i < docStr.length - 1; i++) { + soma += parseInt(docStr.charAt(i), 10) * (docStr.length - i); + } + let resto = soma % 11; + let expectedDigit = (resto === 0 || resto === 1) ? 0 : (11 - resto); + return expectedDigit === checkDigitValue; + } + if (docStr.length === 12) { + let sum = 0; + let isSecond = false; + for (let i = docStr.length - 1; i >= 0; i--) { + let charCode = docStr.charCodeAt(i); + let val = 0; + if (charCode >= 48 && charCode <= 57) val = charCode - 48; + else if (charCode >= 65 && charCode <= 90) val = charCode - 55; + else return false; + + if (isSecond) { + val *= 2; + if (val >= 36) val -= 36; + } + sum += val; + isSecond = !isSecond; + } + return (sum % 36) === 0; + } + return false; + } + const Modal = ({ isOpen, onClose, title, children }) => { if (!isOpen) return null; return ( @@ -241,6 +295,7 @@ 'Receita': 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800', 'Confirmado': 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800', 'Pendente': 'bg-yellow-100 text-yellow-700 border-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400 dark:border-yellow-800', + 'Em Validação': 'bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-900/30 dark:text-orange-400 dark:border-orange-800', 'Em Progresso': 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800', 'Média': 'bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-900/30 dark:text-orange-400 dark:border-orange-800', 'Atrasado': 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800', @@ -286,6 +341,18 @@ const handleSubmit = async (e) => { e.preventDefault(); + setError(''); + + if (!validarNIF(formData.nif)) { + setError('NIF inválido. Verifique o número inserido.'); + return; + } + + if (!validarDocumento(formData.cc)) { + setError('Cartão de Cidadão / BI inválido. Verifique o número inserido.'); + return; + } + if (formData.password !== formData.confirmPassword) { setError('As palavras-passe não coincidem.'); return; @@ -298,6 +365,7 @@ const handleChange = (e) => { setFormData({...formData, [e.target.name]: e.target.value}); + setError(''); }; return ( @@ -643,6 +711,7 @@ const [issues, setIssues] = useState([]); const [bookings, setBookings] = useState([]); const [invoices, setInvoices] = useState([]); + const [faturas, setFaturas] = useState([]); const [messages, setMessages] = useState([]); const [newMessageText, setNewMessageText] = useState(''); const [activeChat, setActiveChat] = useState({ type: 'global', id: 'global', name: 'Fórum do Condomínio' }); @@ -666,12 +735,41 @@ const unsubIssues = loadData('manutencao', setIssues, (a,b) => new Date(b.date) - new Date(a.date)); const unsubBookings = loadData('reservas', setBookings, (a,b) => new Date(a.date) - new Date(b.date)); const unsubInvoices = loadData('faturacao', setInvoices, (a,b) => new Date(b.date) - new Date(a.date)); + const unsubFaturas = loadData('faturas', setFaturas, (a,b) => new Date(b.dataVencimento) - new Date(a.dataVencimento)); return () => { - unsubResidents(); unsubFinances(); unsubIssues(); unsubBookings(); unsubInvoices(); + unsubResidents(); + unsubFinances(); + unsubIssues(); + unsubBookings(); + unsubInvoices(); + unsubFaturas(); }; }, []); + 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') { @@ -691,7 +789,7 @@ return () => unsub(); }, [activeChat, currentUserId]); - const [notificationsList, setNotificationsList] = useState(INITIAL_NOTIFICATIONS); + const [notificationsList, setNotificationsList] = useState([]); const [isNotificationsOpen, setNotificationsOpen] = useState(false); const [activeModal, setActiveModal] = useState(null); @@ -705,6 +803,7 @@ 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({}); @@ -753,14 +852,18 @@ r.unit.toLowerCase().includes(searchQuery.toLowerCase()) ); - const showNotification = (message, type = 'success') => { - setNotification({ message, type }); - const newNotif = { id: Date.now(), message, time: 'Agora', type, read: false }; - setNotificationsList(prev => [newNotif, ...prev]); + const sendSystemNotification = async (message, type = 'info', targetUserId = 'admin') => { + const newNotif = { timestamp: Date.now(), message, time: 'Agora', type, read: false }; + await push(ref(db, `notificacoes/${targetUserId}`), newNotif); }; - const handleClearNotifications = () => { - setNotificationsList([]); + 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); }; @@ -774,6 +877,8 @@ 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; @@ -900,6 +1005,69 @@ } }; + const handleSaveFatura = async (e) => { + e.preventDefault(); + if (!formData.moradorId || !formData.categoria || !formData.valor || !formData.dataVencimento) { + showNotification("Preencha todos os campos obrigatórios.", "error"); + return; + } + try { + const morador = residents.find(r => r.id === formData.moradorId); + if (!morador) return; + + const valor = Number(formData.valor); + const newFaturaRef = push(ref(db, 'faturas')); + await set(newFaturaRef, { + moradorId: morador.id, + nomeMorador: morador.name, + fracao: morador.unit, + categoria: formData.categoria, + valor: valor, + dataVencimento: formData.dataVencimento, + status: 'Pendente', + dataEmissao: new Date().toISOString().split('T')[0] + }); + + const newPending = (Number(morador.pending) || 0) + valor; + await set(ref(db, `condominos/${morador.id}/pending`), newPending); + + showNotification(`Fatura de ${valor.toFixed(2)}€ emitida para ${morador.name}`); + handleCloseModal(); + } catch (error) { + console.error("Erro ao emitir fatura:", error); + showNotification("Erro ao emitir fatura.", "error"); + } + }; + + const handlePayFatura = async (fatura) => { + try { + await set(ref(db, `faturas/${fatura.id}/status`), 'Em Validação'); + sendSystemNotification(`Comprovativo recebido da fração ${fatura.fracao}.`, 'info', 'admin'); + showNotification("Comprovativo enviado! A aguardar validação do administrador.", "success"); + } catch (error) { + console.error("Erro ao pagar fatura:", error); + showNotification("Erro ao processar pagamento.", "error"); + } + }; + + const handleApproveFatura = async (fatura) => { + try { + await set(ref(db, `faturas/${fatura.id}/status`), 'Pago'); + + const morador = residents.find(r => r.id === fatura.moradorId); + if (morador) { + let newPending = (Number(morador.pending) || 0) - Number(fatura.valor); + if (newPending < 0) newPending = 0; + await set(ref(db, `condominos/${morador.id}/pending`), newPending); + } + sendSystemNotification(`O seu pagamento da fatura de ${fatura.categoria} foi aprovado!`, 'success', fatura.moradorId); + 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); @@ -1281,7 +1449,10 @@
-
@@ -1297,7 +1468,10 @@

Autenticação de Dois Fatores (2FA)

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

- +
@@ -1308,7 +1482,10 @@
-
@@ -1440,6 +1617,7 @@ {userRole === 'admin' && { setActiveTab('finance'); setSidebarOpen(false); }} />} {userRole === 'admin' && { setActiveTab('billing'); setSidebarOpen(false); }} />} {userRole === 'admin' && { setActiveTab('approvals'); setSidebarOpen(false); }} />} + {userRole === 'morador' && { setActiveTab('minhas_contas'); setSidebarOpen(false); }} />} { setActiveTab('maintenance'); setSidebarOpen(false); }} /> { setActiveTab('messages'); setSidebarOpen(false); }} /> { setActiveTab('map'); setSidebarOpen(false); }} /> @@ -1692,6 +1870,62 @@ + +
+

Aprovações de Pagamentos

+

Valide ou rejeite pagamentos de faturas enviados pelos condóminos.

+
+
+
+ + + + + + + + + + + {faturas.filter(f => f.status === 'Em Validação').map(fatura => ( + + + + + + + ))} + {faturas.filter(f => f.status === 'Em Validação').length === 0 && ( + + )} + +
MoradorFaturaValorAções
+

{fatura.nomeMorador}

+

Fração: {fatura.fracao}

+
+

{fatura.categoria}

+

Vence: {fatura.dataVencimento}

+
{Number(fatura.valor).toFixed(2)}€ +
+ + +
+
Nenhum pagamento pendente de aprovação.
+
+
)} @@ -1792,8 +2026,8 @@

Avisos de Cobrança

Emita faturas ou avise condóminos individualmente

-
@@ -1816,7 +2050,10 @@
)} + {/* --- MINHAS CONTAS (Morador) --- */} + {activeTab === 'minhas_contas' && userRole === 'morador' && ( +
+
+
+
+

Total Pendente

+

+ {faturas.filter(f => f.moradorId === currentUserId && f.status === 'Pendente').reduce((acc, f) => acc + Number(f.valor), 0).toFixed(2)}€ +

+
+ +
+
+
+

Total Pago

+

+ {faturas.filter(f => f.moradorId === currentUserId && f.status === 'Pago').reduce((acc, f) => acc + Number(f.valor), 0).toFixed(2)}€ +

+
+ +
+
+ +
+
+
+

Minhas Faturas

+

Consulte as suas despesas e faturas emitidas

+
+
+
+ + + + + + + + + + + + + {faturas.filter(f => f.moradorId === currentUserId).length === 0 ? ( + + + + ) : ( + faturas.filter(f => f.moradorId === currentUserId).map((fatura) => ( + + + + + + + + + )) + )} + +
Data EmissãoCategoriaVencimentoValorEstadoAções
+ Nenhuma fatura encontrada. +
{fatura.dataEmissao}{fatura.categoria}{fatura.dataVencimento}{Number(fatura.valor).toFixed(2)}€ + {fatura.status === 'Pendente' ? ( + + ) : fatura.status === 'Em Validação' ? ( + + Em Validação + + ) : ( + + Pago + + )} +
+
+
+
+ )} {/* --- FINANCES --- */} {/* --- FINANCES --- */} @@ -1911,7 +2236,10 @@ {item.type === 'income' ? '+' : '-'}{Number(item.amount).toFixed(2)}€ - @@ -2099,6 +2427,35 @@ + {/* Emitir Fatura Modal */} + +
+
+ + +
+
+ + +
+ + + +
+ {/* Finance Modal */}
diff --git a/test_nif.js b/test_nif.js new file mode 100644 index 0000000..8e79801 --- /dev/null +++ b/test_nif.js @@ -0,0 +1,15 @@ +function validarNIF(nif) { + nif = nif.replace(/\s+/g, ''); + if (!/^\d{9}$/.test(nif)) return false; + if (nif.charAt(0) === '0') return false; + let soma = 0; + for (let i = 0; i < 8; i++) { + soma += parseInt(nif.charAt(i), 10) * (9 - i); + } + const resto = soma % 11; + const digitoControlo = (resto === 0 || resto === 1) ? 0 : (11 - resto); + return digitoControlo === parseInt(nif.charAt(8), 10); +} +console.log(validarNIF('509442013')); // valid NIF (e.g. some company) +console.log(validarNIF('213340058')); // valid random +console.log(validarNIF('200000000')); // invalid