notificaçoes

This commit is contained in:
2026-04-28 17:17:12 +01:00
parent 46c92ead4e
commit 0fe52b0625
2 changed files with 388 additions and 16 deletions

View File

@@ -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 @@
</div>
<InputGroup label="Morada (Sede)" value="Rua das Flores, nº 123, Escritório 2B" />
<div className="flex justify-end mt-6">
<button onClick={() => showNotification('Alterações guardadas com sucesso!', 'success')} className="bg-blue-600 text-white px-6 py-2 rounded-lg font-medium hover:bg-blue-700 shadow-sm transition-colors">
<button onClick={() => {
showNotification('Alterações guardadas com sucesso!', 'success');
sendSystemNotification('Um utilizador atualizou os seus dados pessoais.', 'info', 'admin');
}} 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>
@@ -1297,7 +1468,10 @@
<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')} className="text-orange-900 dark:text-orange-200 text-xs font-bold underline mt-2">Ativar Agora</button>
<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">Ativar Agora</button>
</div>
</div>
@@ -1308,7 +1482,10 @@
<InputGroup label="Confirmar Nova Palavra-passe" type="password" placeholder="Confirmar" />
</div>
<div className="flex justify-end mt-6">
<button onClick={() => showNotification('Segurança atualizada com sucesso!', 'success')} 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">
<button onClick={() => {
showNotification('Segurança atualizada com sucesso!', 'success');
sendSystemNotification('Um utilizador alterou a palavra-passe.', 'info', 'admin');
}} 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>
@@ -1440,6 +1617,7 @@
{userRole === 'admin' && <SidebarItem icon={Wallet} label="Finanças" active={activeTab === 'finance'} onClick={() => { setActiveTab('finance'); setSidebarOpen(false); }} />}
{userRole === 'admin' && <SidebarItem icon={FileText} label="Faturação" active={activeTab === 'billing'} onClick={() => { setActiveTab('billing'); setSidebarOpen(false); }} />}
{userRole === 'admin' && <SidebarItem icon={Users} label="Aprovações" active={activeTab === 'approvals'} onClick={() => { setActiveTab('approvals'); setSidebarOpen(false); }} />}
{userRole === 'morador' && <SidebarItem icon={Wallet} label="Minhas Contas" active={activeTab === 'minhas_contas'} onClick={() => { setActiveTab('minhas_contas'); setSidebarOpen(false); }} />}
<SidebarItem icon={Wrench} label="Manutenção" active={activeTab === 'maintenance'} onClick={() => { setActiveTab('maintenance'); setSidebarOpen(false); }} />
<SidebarItem icon={MessageCircle} label="Mensagens" active={activeTab === 'messages'} onClick={() => { setActiveTab('messages'); setSidebarOpen(false); }} />
<SidebarItem icon={Map} label="Mapa" active={activeTab === 'map'} onClick={() => { setActiveTab('map'); setSidebarOpen(false); }} />
@@ -1692,6 +1870,62 @@
</table>
</div>
</div>
<div className="mt-8 mb-6">
<h2 className="text-2xl font-bold text-slate-800 dark:text-white">Aprovações de Pagamentos</h2>
<p className="text-slate-500 dark:text-dark-mute">Valide ou rejeite pagamentos de faturas enviados 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">Morador</th>
<th className="p-4">Fatura</th>
<th className="p-4">Valor</th>
<th className="p-4 text-center">Ações</th>
</tr>
</thead>
<tbody>
{faturas.filter(f => f.status === 'Em Validação').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">Vence: {fatura.dataVencimento}</p>
</td>
<td className="p-4 font-bold text-slate-800 dark:text-slate-200">{Number(fatura.valor).toFixed(2)}</td>
<td className="p-4">
<div className="flex justify-center gap-2">
<button onClick={() => {
if(window.confirm('Aprovar o pagamento desta fatura?')) {
handleApproveFatura(fatura);
}
}} className="p-2 bg-green-100 text-green-600 rounded hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400" title="Aprovar Pagamento">
<CheckCircle size={18} />
</button>
<button onClick={() => {
if(window.confirm('Rejeitar este pagamento?')) {
set(ref(db, `faturas/${fatura.id}/status`), 'Pendente');
showNotification('Pagamento rejeitado.', 'warning');
}
}} className="p-2 bg-red-100 text-red-600 rounded hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400" title="Rejeitar Pagamento">
<X size={18} />
</button>
</div>
</td>
</tr>
))}
{faturas.filter(f => f.status === 'Em Validação').length === 0 && (
<tr><td colSpan="4" className="p-8 text-center text-slate-500">Nenhum pagamento pendente de aprovação.</td></tr>
)}
</tbody>
</table>
</div>
</div>
</div>
)}
@@ -1792,8 +2026,8 @@
<h3 className="font-bold text-lg text-slate-800 dark:text-white">Avisos de Cobrança</h3>
<p className="text-sm text-slate-500 dark:text-dark-mute">Emita faturas ou avise condóminos individualmente</p>
</div>
<button onClick={() => showNotification("Funcionalidade de Emissão Automática em Breve!", "warning")} 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 Faturas
<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">
@@ -1816,7 +2050,10 @@
</td>
<td className="px-6 py-4 text-center">
<button
onClick={() => showNotification(`Aviso de cobrança enviado para ${resident.email}`, 'success')}
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}
>
@@ -1829,7 +2066,10 @@
Faturar na Hora
</button>
<button
onClick={() => showNotification(`Fatura enviada com sucesso para ${resident.email}`, 'success')}
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
@@ -1842,6 +2082,91 @@
</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">Total Pendente</h4>
<p className="text-2xl font-bold text-slate-800 dark:text-white mt-1">
{faturas.filter(f => f.moradorId === currentUserId && f.status === 'Pendente').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">Total Pago</h4>
<p className="text-2xl font-bold text-slate-800 dark:text-white mt-1">
{faturas.filter(f => f.moradorId === currentUserId && f.status === 'Pago').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">Minhas Faturas</h3>
<p className="text-sm text-slate-500 dark:text-dark-mute">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">Data Emissão</th>
<th className="px-6 py-4">Categoria</th>
<th className="px-6 py-4">Vencimento</th>
<th className="px-6 py-4 text-right">Valor</th>
<th className="px-6 py-4 text-center">Estado</th>
<th className="px-6 py-4 text-center">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">
{fatura.status === 'Pendente' ? (
<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>
) : fatura.status === 'Em Validação' ? (
<span className="text-orange-500 text-xs font-bold flex items-center justify-center gap-1">
<Clock size={14} /> Em Validação
</span>
) : (
<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 --- */}
@@ -1911,7 +2236,10 @@
{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')} className="text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors" title="Descarregar Recibo">
<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="Descarregar Recibo">
<FileText size={16} />
</button>
</td>
@@ -2099,6 +2427,35 @@
</form>
</Modal>
{/* Emitir Fatura Modal */}
<Modal isOpen={activeModal === 'emitir_fatura'} onClose={handleCloseModal} title="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">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="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="Valor (€)" type="number" name="valor" value={formData.valor || ''} onChange={handleInputChange} placeholder="0.00" required />
</div>
<InputGroup label="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="Registar Movimento Financeiro">
<form onSubmit={handleSaveFinance}>