linguagem

This commit is contained in:
2026-05-08 10:41:02 +01:00
parent a7f8e39aae
commit c16fda3fe0
5 changed files with 775 additions and 156 deletions

View File

@@ -1,89 +1,3 @@
<!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">
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
dark: {
bg: '#0f172a',
surface: '#1e293b',
card: '#334155',
border: '#475569',
text: '#f1f5f9',
mute: '#94a3b8'
}
}
}
}
}
{
"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"
}
}
<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>
import React, { useState, useEffect, useRef } from 'react';
import { createRoot } from 'react-dom/client';
import {
@@ -93,10 +7,41 @@
Dumbbell, PartyPopper, Trophy, Map, Calendar, MapPin, Info,
MessageCircle, Paperclip, Send
} from 'lucide-react';
import { app } from './firebase.js';
import { getAuth, signInWithEmailAndPassword, createUserWithEmailAndPassword } from 'https://www.gstatic.com/firebasejs/12.1.0/firebase-auth.js';
import { getDatabase, ref, push, set, onValue, remove, update } from 'https://www.gstatic.com/firebasejs/12.1.0/firebase-database.js';
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' }}>Recarregar Página</button>
</div>
);
}
return this.props.children;
}
}
const auth = getAuth(app);
const db = getDatabase(app);
@@ -303,12 +248,23 @@
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('Email ou Palavra-passe incorreta');
setTimeout(() => {
setError('');
}, 5000);
}
};
@@ -354,8 +310,15 @@
</div>
)}
<div className="space-y-4">
<button type="submit" className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded-lg transition-colors shadow-lg shadow-blue-500/30">
Entrar
<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...
</>
) : (
'Entrar'
)}
</button>
</div>
</form>
@@ -434,7 +397,7 @@
userId = 'admin_001';
} else {
const residentUser = residents.find(r => r.email && r.email.toLowerCase() === email.toLowerCase());
if (residentUser && (password === residentUser.contact || password === '1234')) {
if (residentUser && (password === residentUser.password || (!residentUser.password && password === residentUser.contact) || password === '1234')) {
role = residentUser.role || 'morador';
userName = residentUser.name + (residentUser.unit && residentUser.unit !== 'Pendente' ? ` (${residentUser.unit})` : '');
userId = residentUser.id || userId;
@@ -494,7 +457,17 @@
return onValue(ref(db, path), (snapshot) => {
const data = snapshot.val();
if (data) {
let parsed = Object.entries(data).map(([id, val]) => ({ id, ...val }));
let parsed = Object.entries(data).map(([id, val]) => {
if (path === 'faturas' && val.status === 'Em Validação') {
return { id, ...val, status: 'Pago' };
}
return { id, ...val };
});
if (userRole !== 'admin' && (path === 'manutencao' || path === 'reservas')) {
parsed = parsed.filter(item => item.moradorId === currentUserId);
}
if (sortFunc) parsed = parsed.sort(sortFunc);
setter(parsed);
} else {
@@ -669,8 +642,12 @@
} else if (type === 'emitir_fatura') {
setFormData(initialFaturaForm);
} else if (type === 'booking') {
const baseForm = initialBookingForm;
const baseForm = { ...initialBookingForm };
if (defaultFacility) baseForm.facility = defaultFacility;
// Preenche sempre o nome do utilizador logado por defeito
baseForm.resident = currentUserName;
setFormData(baseForm);
}
};
@@ -715,7 +692,8 @@
try {
if (editingItem) {
const residentRef = ref(db, `condominos/${editingItem.id}`);
await set(residentRef, {
const updatedData = {
...editingItem,
unit: formData.unit || '',
name: formData.name || '',
contact: formData.contact || '',
@@ -723,7 +701,11 @@
status: formData.status || 'Pago',
pending: Number(formData.pending) || 0,
role: formData.role || 'morador'
});
};
if (formData.password) {
updatedData.password = formData.password;
}
await set(residentRef, updatedData);
showNotification(`Condómino ${formData.name} atualizado`);
} else {
const residentsListRef = ref(db, 'condominos');
@@ -733,6 +715,7 @@
name: formData.name || '',
contact: formData.contact || '',
email: formData.email || '',
password: formData.password || '1234',
status: formData.status || 'Pago',
pending: Number(formData.pending) || 0,
role: formData.role || 'morador'
@@ -792,7 +775,7 @@
}
try {
const newIssueRef = push(ref(db, 'manutencao'));
await set(newIssueRef, { ...formData });
await set(newIssueRef, { ...formData, moradorId: currentUserId });
sendSystemNotification(`Nova ocorrência reportada: ${formData.title} (${formData.location})`, 'warning', 'admin');
if (userRole !== 'admin') {
@@ -846,10 +829,17 @@
const handlePayFatura = async (fatura) => {
try {
await set(ref(db, `faturas/${fatura.id}/status`), 'Em Validação');
sendSystemNotification(`O pagamento da sua fatura de ${fatura.categoria} foi submetido. Aguarda validação.`, 'info', fatura.moradorId);
sendSystemNotification(`Comprovativo recebido da fração ${fatura.fracao}.`, 'info', 'admin');
showNotification("Comprovativo enviado! A aguardar validação do administrador.", "success");
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 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");
@@ -900,7 +890,8 @@
const bookingData = {
...formData,
facilityName: facilityNames[formData.facility],
status: 'Confirmado'
status: 'Confirmado',
moradorId: currentUserId
};
const newBookingRef = push(ref(db, 'reservas'));
@@ -1209,6 +1200,110 @@
const ProfileView = ({ theme, setTheme }) => {
const [activeSection, setActiveSection] = useState('personal');
const isMorador = userRole !== 'admin';
const [formData, setFormData] = useState({
name: 'A carregar...',
role: '...',
email: '',
contact: '',
address: ''
});
useEffect(() => {
if (isMorador) {
const currentUserData = residents.find(r => r.id === currentUserId) || {};
setFormData({
name: currentUserData.name || currentUserName || '',
role: `Fração ${currentUserData.unit || 'N/A'}`,
email: currentUserData.email || '',
contact: currentUserData.contact || '',
address: 'Morada do Condomínio'
});
} else {
const adminRef = ref(db, 'configuracoes/admin_profile');
const unsub = onValue(adminRef, (snapshot) => {
if (snapshot.exists()) {
setFormData(snapshot.val());
} else {
setFormData({
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) => {
setFormData(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);
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`), formData.email);
await set(ref(db, `condominos/${currentUserData.id}/contact`), formData.contact);
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'), formData);
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');
}
}
};
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">
@@ -1258,19 +1353,16 @@
<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">Dados Pessoais</h3>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<InputGroup label="Nome Completo" value="Administrador do Condomínio" disabled />
<InputGroup label="Cargo" value="Síndico / Gestor" disabled />
<InputGroup label="Nome Completo" value={formData.name} onChange={(e) => handleChange('name', e.target.value)} disabled={isMorador} />
<InputGroup label={isMorador ? "Fração" : "Cargo"} value={formData.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="Email" value="admin@mycondominium.pt" type="email" />
<InputGroup label="Telefone" value="+351 912 345 678" />
<InputGroup label="Email" value={formData.email} onChange={(e) => handleChange('email', e.target.value)} type="email" />
<InputGroup label="Telefone" value={formData.contact} onChange={(e) => handleChange('contact', e.target.value)} />
</div>
<InputGroup label="Morada (Sede)" value="Rua das Flores, nº 123, Escritório 2B" />
<InputGroup label={isMorador ? "Morada" : "Morada (Sede)"} value={formData.address} onChange={(e) => handleChange('address', e.target.value)} disabled={isMorador} />
<div className="flex justify-end mt-6">
<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">
<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>
@@ -1294,16 +1386,13 @@
</div>
<div className="space-y-4">
<InputGroup label="Palavra-passe Atual" type="password" placeholder="••••••••" />
<InputGroup label="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="Nova Palavra-passe" type="password" placeholder="Min. 8 caracteres" />
<InputGroup label="Confirmar Nova Palavra-passe" type="password" placeholder="Confirmar" />
<InputGroup label="Nova Palavra-passe" type="password" placeholder="Min. 8 caracteres" value={passwordData.new} onChange={(e) => handlePasswordChange('new', e.target.value)} />
<InputGroup label="Confirmar Nova Palavra-passe" type="password" placeholder="Confirmar" value={passwordData.confirm} onChange={(e) => handlePasswordChange('confirm', e.target.value)} />
</div>
<div className="flex justify-end mt-6">
<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">
<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>
@@ -1360,6 +1449,43 @@
</div>
</div>
<div>
<h4 className="text-sm font-bold text-slate-700 dark:text-slate-300 mb-3">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 = match ? match[1] : 'pt';
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">Aparência</h4>
<div className="grid grid-cols-3 gap-4">
@@ -1894,10 +2020,6 @@
>
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
@@ -1950,12 +2072,32 @@
<h3 className="font-bold text-lg text-slate-800 dark:text-white">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>
<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 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">
@@ -2184,7 +2326,10 @@
<InputGroup label="Fração" name="unit" value={formData.unit || ''} onChange={handleInputChange} placeholder="Ex: 1º Esq" required />
<InputGroup label="Nome Completo" name="name" value={formData.name || ''} onChange={handleInputChange} placeholder="Nome do proprietário" required />
<InputGroup label="Email" type="email" name="email" value={formData.email || ''} onChange={handleInputChange} placeholder="email@exemplo.com" />
<InputGroup label="Contacto" name="contact" value={formData.contact || ''} onChange={handleInputChange} placeholder="912 345 678" />
<div className="grid grid-cols-2 gap-4">
<InputGroup label="Telemóvel" name="contact" value={formData.contact || ''} onChange={handleInputChange} placeholder="912 345 678" required />
<InputGroup label="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="Estado" name="status" value={formData.status || 'Pago'} onChange={handleInputChange} options={[{ value: 'Pago', label: 'Pago' }, { value: 'Pendente', label: 'Pendente' }, { value: 'Atrasado', label: 'Atrasado' }]} />
<InputGroup label="Valor Pendente (€)" type="number" name="pending" value={formData.pending || 0} onChange={handleInputChange} />
@@ -2263,7 +2408,7 @@
<InputGroup label="Data" type="date" name="date" value={formData.date || ''} onChange={handleInputChange} required />
<InputGroup label="Horário" name="time" value={formData.time || ''} onChange={handleInputChange} placeholder="Ex: 14:00 - 16:00" required />
</div>
<InputGroup label="Reservado para (Condómino)" name="resident" value={formData.resident || ''} onChange={handleInputChange} placeholder="Nome do residente" required />
<InputGroup label="Reservado para (Condómino)" name="resident" value={formData.resident || ''} onChange={handleInputChange} placeholder="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>
@@ -2329,7 +2474,8 @@
}
const root = createRoot(document.getElementById('root'));
root.render(<App />);
<!-- Firebase configs moved to top in React Module -->
</body>
</html>
root.render(
<ErrorBoundary>
<App />
</ErrorBoundary>
);