correção.geral
This commit is contained in:
398
index.html
398
index.html
@@ -4,7 +4,9 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#0f172a">
|
||||
<title>CondoMaster Pro</title>
|
||||
<link rel="manifest" href="./manifest.json">
|
||||
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
@@ -304,132 +306,7 @@
|
||||
);
|
||||
};
|
||||
|
||||
const WaitingApprovalView = ({ onLogout }) => {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-slate-100 dark:bg-dark-bg p-4 font-sans text-center">
|
||||
<div className="bg-white dark:bg-dark-surface p-8 rounded-xl shadow-2xl w-full max-w-md border border-slate-100 dark:border-dark-border">
|
||||
<div className="inline-flex p-4 bg-orange-100 dark:bg-orange-900/30 rounded-full mb-6 text-orange-600 dark:text-orange-400">
|
||||
<AlertCircle size={48} />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-slate-800 dark:text-white mb-2">Conta Pendente</h2>
|
||||
<p className="text-slate-500 dark:text-gray-400 mb-8">
|
||||
O seu registo foi concluído com sucesso, mas a sua conta aguarda aprovação da administração. Por favor, aguarde.
|
||||
</p>
|
||||
<button onClick={onLogout} className="w-full bg-slate-200 hover:bg-slate-300 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-800 dark:text-white font-bold py-3 rounded-lg transition-colors">
|
||||
Sair
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RegisterView = ({ onToggleView, onRegister }) => {
|
||||
const [formData, setFormData] = useState({
|
||||
name: '', dob: '', phone: '', cc: '', nif: '', unit: '', email: '', password: '', confirmPassword: ''
|
||||
});
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const isFormValid = Object.values(formData).every(val => val.trim() !== '') && formData.password === formData.confirmPassword;
|
||||
|
||||
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;
|
||||
}
|
||||
const result = await onRegister(formData);
|
||||
if (!result.success) {
|
||||
setError(result.message || 'Ocorreu um erro ao tentar registar a conta.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFormData({...formData, [e.target.name]: e.target.value});
|
||||
setError('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-slate-100 dark:bg-dark-bg p-4 py-12 transition-colors duration-300 font-sans">
|
||||
<div className="bg-white dark:bg-dark-surface p-8 rounded-xl shadow-2xl w-full max-w-2xl animate-fade-in border border-slate-100 dark:border-dark-border">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-bold text-slate-800 dark:text-white">Criar Conta</h1>
|
||||
<p className="text-slate-500 dark:text-gray-400 mt-2">Registo de Novo Morador</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="col-span-1 md:col-span-2">
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Nome Completo</label>
|
||||
<input type="text" name="name" value={formData.name} onChange={handleChange} required className="w-full px-4 py-2 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" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Data de Nascimento</label>
|
||||
<input type="date" name="dob" value={formData.dob} onChange={handleChange} required className="w-full px-4 py-2 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" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Nº Telemóvel</label>
|
||||
<input type="tel" name="phone" value={formData.phone} onChange={handleChange} required className="w-full px-4 py-2 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" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Cartão de Cidadão</label>
|
||||
<input type="text" name="cc" value={formData.cc} onChange={handleChange} required className="w-full px-4 py-2 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" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">NIF</label>
|
||||
<input type="text" name="nif" value={formData.nif} onChange={handleChange} required className="w-full px-4 py-2 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" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Fração (Ex: 1º Dto)</label>
|
||||
<input type="text" name="unit" value={formData.unit} onChange={handleChange} required className="w-full px-4 py-2 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" />
|
||||
</div>
|
||||
<div className="col-span-1 md:col-span-2">
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Email</label>
|
||||
<input type="email" name="email" value={formData.email} onChange={handleChange} required className="w-full px-4 py-2 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" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Palavra-passe</label>
|
||||
<input type="password" name="password" value={formData.password} onChange={handleChange} required className="w-full px-4 py-2 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" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Confirmar Palavra-passe</label>
|
||||
<input type="password" name="confirmPassword" value={formData.confirmPassword} onChange={handleChange} required className="w-full px-4 py-2 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" />
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="col-span-1 md:col-span-2 p-3 bg-red-50 text-red-600 text-sm rounded-lg flex items-center gap-2">
|
||||
<AlertCircle size={16} />
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="col-span-1 md:col-span-2 mt-4 space-y-4">
|
||||
<button type="submit" disabled={!isFormValid} className="w-full bg-blue-600 disabled:bg-blue-300 hover:bg-blue-700 text-white font-bold py-3 rounded-lg transition-colors">
|
||||
Registar
|
||||
</button>
|
||||
<div className="text-center">
|
||||
<button type="button" onClick={onToggleView} className="text-sm text-blue-600 hover:underline dark:text-blue-400">
|
||||
Já tem conta? Iniciar Sessão
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LoginView = ({ onLogin, onToggleView }) => {
|
||||
const LoginView = ({ onLogin }) => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
@@ -487,11 +364,6 @@
|
||||
<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>
|
||||
<div className="text-center">
|
||||
<button type="button" onClick={onToggleView} className="text-sm text-blue-600 hover:underline dark:text-blue-400">
|
||||
Ainda não tem conta? Registe-se
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -520,111 +392,7 @@
|
||||
const [userStatus, setUserStatus] = useState(() => {
|
||||
return sessionStorage.getItem('condo_user_status') || 'aprovado';
|
||||
});
|
||||
const [authView, setAuthView] = useState('login');
|
||||
|
||||
const handleRegister = async (data) => {
|
||||
// Verificação na lista atual de moradores para prevenir duplicados locais
|
||||
const emailExists = residents.some(r => r.email && r.email.toLowerCase() === data.email.toLowerCase());
|
||||
if (emailExists) {
|
||||
return { success: false, message: 'Este email já se encontra registado no sistema.' };
|
||||
}
|
||||
|
||||
try {
|
||||
const userCredential = await createUserWithEmailAndPassword(auth, data.email, data.password);
|
||||
const userId = userCredential.user.uid;
|
||||
|
||||
await set(ref(db, `condominos/${userId}`), {
|
||||
id: userId,
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
contact: data.phone,
|
||||
dob: data.dob,
|
||||
cc: data.cc,
|
||||
nif: data.nif,
|
||||
role: 'morador',
|
||||
status: 'pendente',
|
||||
unit: data.unit,
|
||||
pending: 0
|
||||
});
|
||||
|
||||
await push(ref(db, `notificacoes/admin`), {
|
||||
timestamp: Date.now(),
|
||||
message: `Novo pedido de registo: ${data.name} (${data.unit}). A aguardar aprovação.`,
|
||||
time: 'Agora',
|
||||
type: 'info',
|
||||
read: false
|
||||
});
|
||||
|
||||
sessionStorage.setItem('condo_auth', 'true');
|
||||
sessionStorage.setItem('condo_role', 'morador');
|
||||
sessionStorage.setItem('condo_user_name', data.name);
|
||||
sessionStorage.setItem('condo_user_id', userId);
|
||||
sessionStorage.setItem('condo_user_status', 'pendente');
|
||||
|
||||
setIsAuthenticated(true);
|
||||
setUserRole('morador');
|
||||
setCurrentUserName(data.name);
|
||||
setCurrentUserId(userId);
|
||||
setUserStatus('pendente');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Erro no registo Firebase:", error);
|
||||
|
||||
if (error.code === 'auth/email-already-in-use') {
|
||||
return { success: false, message: 'Este email já está associado a outra conta.' };
|
||||
}
|
||||
if (error.code === 'auth/weak-password') {
|
||||
return { success: false, message: 'A palavra-passe deve ter pelo menos 6 caracteres.' };
|
||||
}
|
||||
if (error.code === 'auth/invalid-email') {
|
||||
return { success: false, message: 'O formato do email é inválido.' };
|
||||
}
|
||||
|
||||
// Se falhar devido a falta de configuração, simula registo local (fallback)
|
||||
console.log("A executar fallback local de registo...");
|
||||
const localId = 'local_' + Date.now();
|
||||
|
||||
try {
|
||||
await set(ref(db, `condominos/${localId}`), {
|
||||
id: localId,
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
contact: data.phone,
|
||||
dob: data.dob,
|
||||
cc: data.cc,
|
||||
nif: data.nif,
|
||||
role: 'morador',
|
||||
status: 'pendente',
|
||||
unit: data.unit,
|
||||
pending: 0,
|
||||
contact: data.password // Guardado no campo contact apenas para o fallback mock local de login funcionar
|
||||
});
|
||||
|
||||
await push(ref(db, `notificacoes/admin`), {
|
||||
timestamp: Date.now(),
|
||||
message: `Novo pedido de registo: ${data.name} (${data.unit}). A aguardar aprovação.`,
|
||||
time: 'Agora',
|
||||
type: 'info',
|
||||
read: false
|
||||
});
|
||||
} catch(dbErr) {
|
||||
console.error("Base de dados inacessível no fallback.", dbErr);
|
||||
}
|
||||
|
||||
sessionStorage.setItem('condo_auth', 'true');
|
||||
sessionStorage.setItem('condo_role', 'morador');
|
||||
sessionStorage.setItem('condo_user_name', data.name);
|
||||
sessionStorage.setItem('condo_user_id', localId);
|
||||
sessionStorage.setItem('condo_user_status', 'pendente');
|
||||
|
||||
setIsAuthenticated(true);
|
||||
setUserRole('morador');
|
||||
setCurrentUserName(data.name);
|
||||
setCurrentUserId(localId);
|
||||
setUserStatus('pendente');
|
||||
return { success: true };
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogin = async (email, password) => {
|
||||
try {
|
||||
@@ -723,6 +491,10 @@
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [newMessageText, setNewMessageText] = useState('');
|
||||
const [activeChat, setActiveChat] = useState({ type: 'global', id: 'global', name: 'Fórum do Condomínio' });
|
||||
const [chatGroups, setChatGroups] = useState([]);
|
||||
const [isCreateGroupModalOpen, setIsCreateGroupModalOpen] = useState(false);
|
||||
const [newGroupName, setNewGroupName] = useState('');
|
||||
const [newGroupMembers, setNewGroupMembers] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = (path, setter, sortFunc = null) => {
|
||||
@@ -744,6 +516,7 @@
|
||||
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));
|
||||
const unsubGroups = loadData('grupos_chat', setChatGroups);
|
||||
|
||||
return () => {
|
||||
unsubResidents();
|
||||
@@ -752,6 +525,7 @@
|
||||
unsubBookings();
|
||||
unsubInvoices();
|
||||
unsubFaturas();
|
||||
unsubGroups();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -782,6 +556,8 @@
|
||||
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) => {
|
||||
@@ -1627,13 +1403,7 @@
|
||||
};
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return authView === 'login'
|
||||
? <LoginView onLogin={handleLogin} onToggleView={() => setAuthView('register')} />
|
||||
: <RegisterView onRegister={handleRegister} onToggleView={() => setAuthView('login')} />;
|
||||
}
|
||||
|
||||
if (userStatus === 'pendente') {
|
||||
return <WaitingApprovalView onLogout={handleLogout} />;
|
||||
return <LoginView onLogin={handleLogin} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -1665,7 +1435,8 @@
|
||||
{userRole === 'admin' && <SidebarItem icon={Users} label="Condóminos" active={activeTab === 'residents'} onClick={() => { setActiveTab('residents'); setSidebarOpen(false); }} />}
|
||||
{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 === 'admin' && <SidebarItem icon={CheckCircle} label="Pagamentos" 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); }} />
|
||||
@@ -1864,72 +1635,10 @@
|
||||
</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">Aprovações de Moradores</h2>
|
||||
<p className="text-slate-500 dark:text-dark-mute">Valide ou rejeite os novos pedidos de registo na plataforma.</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">NIF / CC</th>
|
||||
<th className="p-4">Contacto</th>
|
||||
<th className="p-4">Data Nasc.</th>
|
||||
<th className="p-4 text-center">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{residents.filter(r => r.status === 'pendente').map(req => (
|
||||
<tr key={req.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">{req.name}</p>
|
||||
<p className="text-xs text-slate-400">{req.email}</p>
|
||||
</td>
|
||||
<td className="p-4 text-slate-600 dark:text-slate-400">
|
||||
<p className="text-sm">NIF: {req.nif}</p>
|
||||
<p className="text-sm">CC: {req.cc}</p>
|
||||
</td>
|
||||
<td className="p-4 text-sm text-slate-600 dark:text-slate-400">{req.contact}</td>
|
||||
<td className="p-4 text-sm text-slate-600 dark:text-slate-400">{req.dob}</td>
|
||||
<td className="p-4">
|
||||
<div className="flex justify-center gap-2">
|
||||
<button onClick={() => {
|
||||
if(window.confirm('Aprovar este morador?')) {
|
||||
set(ref(db, `condominos/${req.id}/status`), 'aprovado');
|
||||
sendSystemNotification(`O registo do morador ${req.name} (${req.unit || ''}) foi aprovado.`, 'success', 'admin');
|
||||
sendSystemNotification(`A sua conta foi aprovada pela administração! Bem-vindo(a).`, 'success', req.id);
|
||||
showNotification('Morador aprovado com sucesso!', 'success');
|
||||
}
|
||||
}} 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">
|
||||
<CheckCircle size={18} />
|
||||
</button>
|
||||
<button onClick={() => {
|
||||
if(window.confirm('Rejeitar este pedido? O registo será eliminado.')) {
|
||||
remove(ref(db, `condominos/${req.id}`));
|
||||
showNotification('Registo eliminado.', 'success');
|
||||
}
|
||||
}} 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">
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{residents.filter(r => r.status === 'pendente').length === 0 && (
|
||||
<tr><td colSpan="5" className="p-8 text-center text-slate-500">Nenhum pedido pendente de aprovação.</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</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>
|
||||
@@ -2317,7 +2026,7 @@
|
||||
<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">Conversas</h3>
|
||||
<button onClick={() => showNotification('Criação de novos grupos em breve!', 'warning')} 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">
|
||||
<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="Criar Grupo">
|
||||
<Plus size={18} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -2345,6 +2054,26 @@
|
||||
</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">Grupo</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{residents.filter(r => r.id !== currentUserId).map(res => (
|
||||
<div
|
||||
key={res.id}
|
||||
@@ -2372,12 +2101,12 @@
|
||||
<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' : 'bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-300'}`}>
|
||||
{activeChat.type === 'global' ? <Users size={20} /> : activeChat.name.substring(0, 2).toUpperCase()}
|
||||
<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' : 'Privado'}</p>
|
||||
<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>
|
||||
@@ -2414,7 +2143,9 @@
|
||||
try {
|
||||
const path = activeChat.type === 'global'
|
||||
? 'mural_mensagens'
|
||||
: `mensagens_privadas/${[currentUserId, activeChat.id].sort().join('_')}`;
|
||||
: activeChat.type === 'group'
|
||||
? `mensagens_grupo/${activeChat.id}`
|
||||
: `mensagens_privadas/${[currentUserId, activeChat.id].sort().join('_')}`;
|
||||
|
||||
const newMsgRef = push(ref(db, path));
|
||||
await set(newMsgRef, {
|
||||
@@ -2566,6 +2297,53 @@
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
<Modal isOpen={isCreateGroupModalOpen} onClose={() => setIsCreateGroupModalOpen(false)} title="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="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">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>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user