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 @@
Recomendamos ativar o 2FA para maior segurança da sua conta.
-Valide ou rejeite pagamentos de faturas enviados pelos condóminos.
+| Morador | +Fatura | +Valor | +Ações | +
|---|---|---|---|
|
+ {fatura.nomeMorador} +Fração: {fatura.fracao} + |
+
+ {fatura.categoria} +Vence: {fatura.dataVencimento} + |
+ {Number(fatura.valor).toFixed(2)}€ | +
+
+
+ |
+
| Nenhum pagamento pendente de aprovação. | |||
Emita faturas ou avise condóminos individualmente
-+ {faturas.filter(f => f.moradorId === currentUserId && f.status === 'Pendente').reduce((acc, f) => acc + Number(f.valor), 0).toFixed(2)}€ +
++ {faturas.filter(f => f.moradorId === currentUserId && f.status === 'Pago').reduce((acc, f) => acc + Number(f.valor), 0).toFixed(2)}€ +
+Consulte as suas despesas e faturas emitidas
+| Data Emissão | +Categoria | +Vencimento | +Valor | +Estado | +Ações | +
|---|---|---|---|---|---|
| + Nenhuma fatura encontrada. + | +|||||
| {fatura.dataEmissao} | +{fatura.categoria} | +{fatura.dataVencimento} | +{Number(fatura.valor).toFixed(2)}€ | +
+ {fatura.status === 'Pendente' ? (
+ |
+ |