1660 lines
128 KiB
HTML
1660 lines
128 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="pt">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>CondoMaster Pro</title>
|
|
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
darkMode: 'class',
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
dark: {
|
|
bg: '#0f172a',
|
|
surface: '#1e293b',
|
|
card: '#334155',
|
|
border: '#475569',
|
|
text: '#f1f5f9',
|
|
mute: '#94a3b8'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<script type="importmap">
|
|
{
|
|
"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"
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
|
|
<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>
|
|
|
|
<script type="text/babel" data-type="module">
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import {
|
|
Building2, Users, Wallet, Wrench, Bell, Search, Plus, Menu, X,
|
|
TrendingUp, TrendingDown, CheckCircle, AlertCircle, Clock, LogOut,
|
|
Edit2, Trash2, Save, Filter, MoreVertical, FileText,
|
|
Dumbbell, PartyPopper, Trophy, Map, Calendar, MapPin, Info,
|
|
MessageCircle, Paperclip, Send
|
|
} from 'lucide-react';
|
|
|
|
const INITIAL_RESIDENTS = [
|
|
{ id: 1, unit: '1º Esq', name: 'Ana Silva', contact: '912 345 678', email: 'ana.silva@email.com', status: 'Pago', pending: 0, role: 'morador' },
|
|
{ id: 2, unit: '1º Dto', name: 'Carlos Santos', contact: '965 432 109', email: 'carlos.s@email.com', status: 'Pendente', pending: 45.00, role: 'morador' },
|
|
{ id: 3, unit: '2º Esq', name: 'Maria Pereira', contact: '933 221 110', email: 'maria.p@email.com', status: 'Pago', pending: 0, role: 'morador' },
|
|
{ id: 4, unit: '2º Dto', name: 'João Ferreira', contact: '918 765 432', email: 'joao.f@email.com', status: 'Atrasado', pending: 135.00, role: 'morador' },
|
|
{ id: 5, unit: '3º Esq', name: 'Sofia Costa', contact: '922 334 455', email: 'sofia.c@email.com', status: 'Pago', pending: 0, role: 'morador' },
|
|
];
|
|
|
|
const INITIAL_FINANCES = [
|
|
{ id: 1, type: 'income', category: 'Quotas Mensais', date: '2023-10-01', amount: 2250.00, desc: 'Pagamento de quotas Outubro' },
|
|
{ id: 2, type: 'expense', category: 'Limpeza', date: '2023-10-02', amount: 450.00, desc: 'Serviço de Limpeza Semanal' },
|
|
{ id: 3, type: 'expense', category: 'Elevadores', date: '2023-10-05', amount: 120.00, desc: 'Manutenção Mensal' },
|
|
{ id: 4, type: 'income', category: 'Aluguer Salão', date: '2023-10-10', amount: 50.00, desc: 'Reserva 2º Dto' },
|
|
{ id: 5, type: 'expense', category: 'Jardinagem', date: '2023-10-12', amount: 85.00, desc: 'Poda de árvores' },
|
|
];
|
|
|
|
const INITIAL_ISSUES = [
|
|
{ id: 1, title: 'Lâmpada fundida no Hall', location: 'R/C', status: 'Novo', priority: 'Baixa', date: '2023-10-15' },
|
|
{ id: 2, title: 'Porta da garagem não fecha', location: 'Garagem -1', status: 'Em Progresso', priority: 'Alta', date: '2023-10-14' },
|
|
{ id: 3, title: 'Infiltração no teto', location: '3º Dto', status: 'Resolvido', priority: 'Média', date: '2023-10-10' },
|
|
];
|
|
|
|
const INITIAL_BOOKINGS = [
|
|
{ id: 1, facility: 'hall', facilityName: 'Salão de Festas', date: '2023-10-25', time: '14:00 - 20:00', resident: 'Ana Silva', status: 'Confirmado', cost: 50 },
|
|
{ id: 2, facility: 'gym', facilityName: 'Ginásio', date: '2023-10-20', time: '09:00 - 10:00', resident: 'Carlos Santos', status: 'Confirmado', cost: 0 },
|
|
{ id: 3, facility: 'park', facilityName: 'Parque de Jogos', date: '2023-10-22', time: '18:00 - 19:00', resident: 'Sofia Costa', status: 'Pendente', cost: 10 },
|
|
];
|
|
|
|
const INITIAL_NOTIFICATIONS = [
|
|
{ id: 1, message: 'Nova reserva: Salão de Festas (25 Out)', time: 'Há 1 hora', type: 'info', read: false },
|
|
{ id: 2, message: 'Nova quota paga: Maria Pereira', time: 'Há 2 horas', type: 'success', read: false },
|
|
{ id: 3, message: 'Manutenção urgente reportada', time: 'Há 5 horas', type: 'warning', read: false },
|
|
];
|
|
|
|
const Modal = ({ isOpen, onClose, title, children }) => {
|
|
if (!isOpen) return null;
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in">
|
|
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-2xl w-full max-w-lg overflow-hidden animate-slide-up transition-colors">
|
|
<div className="flex justify-between items-center p-6 border-b border-slate-100 dark:border-dark-border bg-slate-50 dark:bg-dark-bg">
|
|
<h3 className="font-bold text-lg text-slate-800 dark:text-white">{title}</h3>
|
|
<button onClick={onClose} className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
|
<X size={24} />
|
|
</button>
|
|
</div>
|
|
<div className="p-6 max-h-[80vh] overflow-y-auto">{children}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const InputGroup = ({ label, name, type = 'text', value, onChange, placeholder, required = false, options = null, disabled = false }) => (
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
|
{label} {required && <span className="text-red-500">*</span>}
|
|
</label>
|
|
{options ? (
|
|
<select
|
|
name={name}
|
|
value={value}
|
|
onChange={onChange}
|
|
disabled={disabled}
|
|
className="w-full px-3 py-2 border border-slate-300 dark:border-dark-border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-dark-card dark:text-white disabled:bg-slate-100 dark:disabled:bg-slate-800 disabled:text-slate-500 dark:disabled:text-slate-400 transition-colors"
|
|
>
|
|
{options.map(opt => (
|
|
<option key={opt.value} value={opt.value} className="dark:bg-dark-bg">{opt.label}</option>
|
|
))}
|
|
</select>
|
|
) : (
|
|
<input
|
|
type={type}
|
|
name={name}
|
|
value={value}
|
|
onChange={onChange}
|
|
placeholder={placeholder}
|
|
required={required}
|
|
disabled={disabled}
|
|
className="w-full px-3 py-2 border border-slate-300 dark:border-dark-border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-dark-card dark:text-white dark:placeholder-slate-500 disabled:bg-slate-100 dark:disabled:bg-slate-800 disabled:text-slate-500 dark:disabled:text-slate-400 transition-colors"
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
const SidebarItem = ({ icon: Icon, label, active, onClick }) => (
|
|
<button
|
|
onClick={onClick}
|
|
className={`flex items-center w-full gap-3 px-4 py-3 text-sm font-medium transition-all rounded-lg ${active
|
|
? 'bg-blue-600 text-white shadow-md translate-x-1'
|
|
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-dark-card hover:text-blue-600 dark:hover:text-blue-400'
|
|
}`}
|
|
>
|
|
<Icon size={20} />
|
|
<span>{label}</span>
|
|
</button>
|
|
);
|
|
|
|
const Card = ({ title, value, icon: Icon, trend, trendValue, color, subtitle }) => (
|
|
<div className="bg-white dark:bg-dark-surface p-6 rounded-xl shadow-sm border border-slate-100 dark:border-dark-border hover:shadow-md transition-all">
|
|
<div className="flex justify-between items-start">
|
|
<div>
|
|
<p className="text-sm font-medium text-slate-500 dark:text-dark-mute">{title}</p>
|
|
<h3 className="text-2xl font-bold text-slate-800 dark:text-white mt-2">{value}</h3>
|
|
</div>
|
|
<div className={`p-3 rounded-lg ${color}`}>
|
|
<Icon size={24} className="text-white" />
|
|
</div>
|
|
</div>
|
|
<div className="mt-4 flex items-center text-sm justify-between">
|
|
<div className="flex items-center">
|
|
{trend === 'up' ? (
|
|
<span className="text-green-500 flex items-center font-medium bg-green-50 dark:bg-green-900/20 px-2 py-0.5 rounded-full">
|
|
<TrendingUp size={14} className="mr-1" /> {trendValue}
|
|
</span>
|
|
) : trend === 'down' ? (
|
|
<span className="text-red-500 flex items-center font-medium bg-red-50 dark:bg-red-900/20 px-2 py-0.5 rounded-full">
|
|
<TrendingDown size={14} className="mr-1" /> {trendValue}
|
|
</span>
|
|
) : (
|
|
<span className="text-slate-400 dark:text-slate-500 font-medium bg-slate-50 dark:bg-dark-card px-2 py-0.5 rounded-full">
|
|
—
|
|
</span>
|
|
)}
|
|
</div>
|
|
<span className="text-slate-400 dark:text-dark-mute text-xs">{subtitle || 'vs. mês passado'}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const Badge = ({ status }) => {
|
|
const styles = {
|
|
'Pago': 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800',
|
|
'Em dia': 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800',
|
|
'Resolvido': 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800',
|
|
'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 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',
|
|
'Despesa': 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800',
|
|
'Alta': 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800',
|
|
'Novo': 'bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-900/30 dark:text-purple-400 dark:border-purple-800',
|
|
'Baixa': 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800',
|
|
};
|
|
|
|
return (
|
|
<span className={`px-2.5 py-1 rounded-full text-xs font-semibold border ${styles[status] || 'bg-gray-100 text-gray-700 border-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700'}`}>
|
|
{status}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
const LoginView = ({ onLogin }) => {
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [error, setError] = useState('');
|
|
|
|
const handleSubmit = async (e) => {
|
|
e.preventDefault();
|
|
const success = await onLogin(email, password);
|
|
if (!success) {
|
|
setError('Email ou Palavra-passe incorreta');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-slate-100 dark:bg-dark-bg p-4 transition-colors duration-300 font-sans">
|
|
<div className="bg-white dark:bg-dark-surface p-8 rounded-xl shadow-2xl w-full max-w-md animate-fade-in border border-slate-100 dark:border-dark-border">
|
|
<div className="text-center mb-8">
|
|
<div className="inline-flex p-3 bg-blue-100 dark:bg-blue-900/30 rounded-full mb-4 text-blue-600 dark:text-blue-400">
|
|
<Building2 size={32} />
|
|
</div>
|
|
<h1 className="text-2xl font-bold text-slate-800 dark:text-white">CondoMaster<span className="text-blue-600">Pro</span></h1>
|
|
<p className="text-slate-500 dark:text-gray-400 mt-2">Portal de Gestão</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Email</label>
|
|
<input
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
className="w-full px-4 py-3 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 transition-colors"
|
|
placeholder="Endereço de email"
|
|
autoFocus
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Palavra-passe</label>
|
|
<input
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
className="w-full px-4 py-3 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 transition-colors"
|
|
placeholder="Senha de acesso"
|
|
required
|
|
/>
|
|
</div>
|
|
{error && (
|
|
<div className="p-3 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-sm rounded-lg flex items-center gap-2 animate-fade-in">
|
|
<AlertCircle size={16} />
|
|
{error}
|
|
</div>
|
|
)}
|
|
<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>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
function App() {
|
|
const [activeTab, setActiveTab] = useState('dashboard');
|
|
const [isSidebarOpen, setSidebarOpen] = useState(false);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [theme, setTheme] = useState('system');
|
|
|
|
const [isAuthenticated, setIsAuthenticated] = useState(() => {
|
|
return sessionStorage.getItem('condo_auth') === 'true';
|
|
});
|
|
const [userRole, setUserRole] = useState(() => {
|
|
return sessionStorage.getItem('condo_role') || 'morador';
|
|
});
|
|
|
|
const handleLogin = async (email, password) => {
|
|
try {
|
|
const userCredential = await signInWithEmailAndPassword(auth, email, password);
|
|
let role = 'morador';
|
|
if (email.toLowerCase().includes('admin')) {
|
|
role = 'admin';
|
|
} else {
|
|
const residentUser = residents.find(r => r.email && r.email.toLowerCase() === email.toLowerCase());
|
|
if (residentUser) {
|
|
role = residentUser.role || 'morador';
|
|
}
|
|
}
|
|
sessionStorage.setItem('condo_auth', 'true');
|
|
sessionStorage.setItem('condo_role', role);
|
|
setIsAuthenticated(true);
|
|
setUserRole(role);
|
|
return true;
|
|
} catch (error) {
|
|
console.log("Firebase Auth falhou, a tentar conta local...", error);
|
|
let role = null;
|
|
if (email === 'administradores@gmail.com' && password === 'admin123') {
|
|
role = 'admin';
|
|
} else if (email === 'moradores@gmail.com' && password === 'moradores123') {
|
|
role = 'morador';
|
|
} else {
|
|
const residentUser = residents.find(r => r.email && r.email.toLowerCase() === email.toLowerCase());
|
|
if (residentUser && (password === residentUser.contact || password === '1234')) {
|
|
role = residentUser.role || 'morador';
|
|
}
|
|
}
|
|
|
|
if (role) {
|
|
sessionStorage.setItem('condo_auth', 'true');
|
|
sessionStorage.setItem('condo_role', role);
|
|
setIsAuthenticated(true);
|
|
setUserRole(role);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const handleLogout = () => {
|
|
if (window.confirm('Tem a certeza que deseja terminar sessão?')) {
|
|
sessionStorage.removeItem('condo_auth');
|
|
sessionStorage.removeItem('condo_role');
|
|
setIsAuthenticated(false);
|
|
setUserRole(null);
|
|
setActiveTab('dashboard');
|
|
}
|
|
};
|
|
|
|
const [residents, setResidents] = useState(INITIAL_RESIDENTS);
|
|
const [finances, setFinances] = useState(INITIAL_FINANCES);
|
|
const [issues, setIssues] = useState(INITIAL_ISSUES);
|
|
const [bookings, setBookings] = useState(INITIAL_BOOKINGS);
|
|
const [notificationsList, setNotificationsList] = useState(INITIAL_NOTIFICATIONS);
|
|
const [isNotificationsOpen, setNotificationsOpen] = useState(false);
|
|
|
|
const [activeModal, setActiveModal] = useState(null);
|
|
const [editingItem, setEditingItem] = useState(null);
|
|
|
|
const [notification, setNotification] = useState(null);
|
|
|
|
const notificationRef = useRef(null);
|
|
|
|
const initialResidentForm = { unit: '', name: '', contact: '', email: '', status: 'Pago', pending: 0, role: 'morador' };
|
|
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 [formData, setFormData] = useState({});
|
|
|
|
useEffect(() => {
|
|
const root = window.document.documentElement;
|
|
root.classList.remove('dark');
|
|
|
|
if (theme === 'dark') {
|
|
root.classList.add('dark');
|
|
} else if (theme === 'system') {
|
|
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
root.classList.add('dark');
|
|
}
|
|
}
|
|
}, [theme]);
|
|
|
|
useEffect(() => {
|
|
if (notification) {
|
|
const timer = setTimeout(() => setNotification(null), 3000);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [notification]);
|
|
|
|
useEffect(() => {
|
|
function handleClickOutside(event) {
|
|
if (notificationRef.current && !notificationRef.current.contains(event.target)) {
|
|
setNotificationsOpen(false);
|
|
}
|
|
}
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
return () => {
|
|
document.removeEventListener("mousedown", handleClickOutside);
|
|
};
|
|
}, [notificationRef]);
|
|
|
|
const toggleSidebar = () => setSidebarOpen(!isSidebarOpen);
|
|
|
|
const totalIncome = finances.filter(f => f.type === 'income').reduce((acc, curr) => acc + Number(curr.amount), 0);
|
|
const totalExpense = finances.filter(f => f.type === 'expense').reduce((acc, curr) => acc + Number(curr.amount), 0);
|
|
const balance = totalIncome - totalExpense;
|
|
const activeIssuesCount = issues.filter(i => i.status !== 'Resolvido').length;
|
|
const unreadNotifications = notificationsList.filter(n => !n.read).length;
|
|
|
|
const filteredResidents = residents.filter(r =>
|
|
r.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
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 handleClearNotifications = () => {
|
|
setNotificationsList([]);
|
|
setNotificationsOpen(false);
|
|
};
|
|
|
|
const handleOpenModal = (type, item = null, defaultFacility = null) => {
|
|
setEditingItem(item);
|
|
setActiveModal(type);
|
|
|
|
if (type === 'resident') {
|
|
setFormData(item || initialResidentForm);
|
|
} else if (type === 'finance') {
|
|
setFormData(initialFinanceForm);
|
|
} else if (type === 'issue') {
|
|
setFormData(initialIssueForm);
|
|
} else if (type === 'booking') {
|
|
const baseForm = initialBookingForm;
|
|
if (defaultFacility) baseForm.facility = defaultFacility;
|
|
setFormData(baseForm);
|
|
}
|
|
};
|
|
|
|
const handleCloseModal = () => {
|
|
setActiveModal(null);
|
|
setEditingItem(null);
|
|
setFormData({});
|
|
};
|
|
|
|
const handleInputChange = (e) => {
|
|
const { name, value } = e.target;
|
|
setFormData(prev => {
|
|
const newData = { ...prev, [name]: value };
|
|
if (name === 'facility' || name === 'time') {
|
|
let cost = 0;
|
|
if (newData.facility === 'hall') cost = 50;
|
|
if (newData.facility === 'park') cost = 10;
|
|
newData.cost = cost;
|
|
}
|
|
return newData;
|
|
});
|
|
};
|
|
|
|
const handleToggleRole = (id) => {
|
|
setResidents(residents.map(r => r.id === id ? { ...r, role: r.role === 'admin' ? 'morador' : 'admin' } : r));
|
|
showNotification('Permissões de utilizador atualizadas', 'success');
|
|
};
|
|
|
|
const handleSaveResident = (e) => {
|
|
e.preventDefault();
|
|
if (editingItem) {
|
|
setResidents(residents.map(r => r.id === editingItem.id ? { ...formData, id: r.id } : r));
|
|
showNotification(`Condómino ${formData.name} atualizado`);
|
|
} else {
|
|
setResidents([...residents, { ...formData, id: Date.now(), pending: Number(formData.pending) }]);
|
|
showNotification(`Novo condómino ${formData.name} adicionado`);
|
|
}
|
|
handleCloseModal();
|
|
};
|
|
|
|
const handleDeleteResident = (id) => {
|
|
if (window.confirm('Tem a certeza que deseja eliminar este condómino?')) {
|
|
setResidents(residents.filter(r => r.id !== id));
|
|
showNotification('Condómino removido', 'error');
|
|
}
|
|
};
|
|
|
|
const handleSaveFinance = (e) => {
|
|
e.preventDefault();
|
|
const amount = Number(formData.amount);
|
|
const newTransaction = { ...formData, id: Date.now(), amount };
|
|
setFinances([newTransaction, ...finances]);
|
|
showNotification(`Movimento de ${amount}€ registado`);
|
|
handleCloseModal();
|
|
};
|
|
|
|
const handleSaveIssue = (e) => {
|
|
e.preventDefault();
|
|
const newIssue = { ...formData, id: Date.now() };
|
|
setIssues([newIssue, ...issues]);
|
|
showNotification('Nova ocorrência reportada', 'warning');
|
|
handleCloseModal();
|
|
};
|
|
|
|
const handleResolveIssue = (id) => {
|
|
setIssues(issues.map(i => i.id === id ? { ...i, status: 'Resolvido' } : i));
|
|
showNotification('Ocorrência resolvida com sucesso');
|
|
};
|
|
|
|
const handleSaveBooking = (e) => {
|
|
e.preventDefault();
|
|
const facilityNames = { 'gym': 'Ginásio', 'hall': 'Salão de Festas', 'park': 'Parque de Jogos' };
|
|
const newBooking = {
|
|
...formData,
|
|
id: Date.now(),
|
|
facilityName: facilityNames[formData.facility],
|
|
status: 'Confirmado'
|
|
};
|
|
setBookings([newBooking, ...bookings]);
|
|
|
|
if (newBooking.cost > 0) {
|
|
const newIncome = {
|
|
id: Date.now() + 1,
|
|
type: 'income',
|
|
category: `Reserva: ${newBooking.facilityName}`,
|
|
date: newBooking.date,
|
|
amount: newBooking.cost,
|
|
desc: `Reserva por ${newBooking.resident}`
|
|
};
|
|
setFinances(prev => [newIncome, ...prev]);
|
|
}
|
|
|
|
showNotification(`Reserva confirmada para ${newBooking.facilityName}`);
|
|
handleCloseModal();
|
|
}
|
|
|
|
const DashboardView = () => (
|
|
<div className="space-y-6 animate-fade-in">
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
{userRole === 'admin' ? (
|
|
<Card title="Saldo Disponível" value={`${balance.toFixed(2)}€`} icon={Wallet} trend={balance >= 0 ? 'up' : 'down'} trendValue="Atual" color="bg-blue-500" />
|
|
) : (
|
|
<Card title="As Minhas Quotas" value="Em Dia" icon={CheckCircle} trend="up" trendValue="Pago" color="bg-green-500" subtitle="Sem valores pendentes" />
|
|
)}
|
|
<Card title="Reservas (Mês)" value={bookings.length} icon={Calendar} trend="up" trendValue="+2" color="bg-purple-500" subtitle="Total agendado" />
|
|
<Card title="Manutenções Ativas" value={activeIssuesCount} icon={Wrench} trend="up" trendValue="Novas" color="bg-orange-500" subtitle="Em resolução" />
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border flex flex-col h-full transition-colors">
|
|
<div className="p-6 border-b border-slate-100 dark:border-dark-border flex justify-between items-center">
|
|
<h3 className="font-bold text-lg text-slate-800 dark:text-white">Próximas Reservas</h3>
|
|
<button className="text-blue-600 dark:text-blue-400 text-sm font-medium" onClick={() => setActiveTab('all_bookings')}>Ver todas as Reservas</button>
|
|
</div>
|
|
<div className="p-4 space-y-3">
|
|
{bookings.slice(0, 4).map(booking => (
|
|
<div key={booking.id} className="flex items-center justify-between p-3 border border-slate-100 dark:border-dark-border rounded-lg hover:bg-slate-50 dark:hover:bg-dark-card transition-colors">
|
|
<div className="flex items-center gap-3">
|
|
<div className={`p-2 rounded-lg ${booking.facility === 'gym' ? 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400' : booking.facility === 'hall' ? 'bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400' : 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400'}`}>
|
|
{booking.facility === 'gym' ? <Dumbbell size={18} /> : booking.facility === 'hall' ? <PartyPopper size={18} /> : <Trophy size={18} />}
|
|
</div>
|
|
<div>
|
|
<p className="font-bold text-sm text-slate-800 dark:text-slate-200">{booking.facilityName}</p>
|
|
<p className="text-xs text-slate-500 dark:text-slate-400">{booking.date} • {booking.time}</p>
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-sm font-medium text-slate-700 dark:text-slate-300">{booking.resident}</p>
|
|
<Badge status={booking.status} />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border flex flex-col h-full transition-colors">
|
|
<div className="p-6 border-b border-slate-100 dark:border-dark-border flex justify-between items-center">
|
|
<h3 className="font-bold text-lg text-slate-800 dark:text-white">Quadro de Avisos</h3>
|
|
<button className="text-sm text-blue-600 dark:text-blue-400" onClick={() => setActiveTab('maintenance')}>{userRole === 'admin' ? 'Gerir' : 'Ver Ocorrências'}</button>
|
|
</div>
|
|
<div className="p-4 space-y-3 overflow-y-auto max-h-[350px]">
|
|
{issues.slice(0, 3).map((issue) => (
|
|
<div key={issue.id} className="border border-slate-100 dark:border-dark-border rounded-lg p-4 bg-slate-50 dark:bg-dark-card transition-colors">
|
|
<div className="flex justify-between mb-2">
|
|
<Badge status={issue.priority} />
|
|
<span className="text-xs text-slate-400 dark:text-slate-500">{issue.date}</span>
|
|
</div>
|
|
<h4 className="font-semibold text-slate-800 dark:text-slate-200">{issue.title}</h4>
|
|
<p className="text-xs text-slate-500 dark:text-dark-mute mt-1">{issue.location}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const BookingView = ({ facilityType, title, icon: Icon, description, priceInfo, color }) => {
|
|
const facilityBookings = bookings.filter(b => b.facility === facilityType);
|
|
|
|
return (
|
|
<div className="animate-fade-in space-y-6">
|
|
<div className={`bg-white dark:bg-dark-surface p-6 rounded-xl shadow-sm border border-slate-100 dark:border-dark-border flex flex-col md:flex-row gap-6 items-center ${color}`}>
|
|
<div className="bg-white/20 p-6 rounded-2xl backdrop-blur-sm text-white">
|
|
<Icon size={48} />
|
|
</div>
|
|
<div className="flex-1 text-white">
|
|
<h2 className="text-2xl font-bold">{title}</h2>
|
|
<p className="opacity-90 mt-1">{description}</p>
|
|
<div className="mt-4 flex gap-4 text-sm font-medium bg-black/10 w-fit px-4 py-2 rounded-lg">
|
|
<span className="flex items-center gap-2"><Clock size={16} /> Horário: 08:00 - 22:00</span>
|
|
<span className="flex items-center gap-2"><Wallet size={16} /> {priceInfo}</span>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => handleOpenModal('booking', null, facilityType)}
|
|
className="bg-white text-slate-900 px-6 py-3 rounded-lg font-bold hover:bg-slate-50 transition-colors shadow-lg flex items-center gap-2"
|
|
>
|
|
<Plus size={20} /> Reservar Agora
|
|
</button>
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-dark-surface rounded-xl shadow-sm border border-slate-100 dark:border-dark-border p-6 transition-colors">
|
|
<h3 className="font-bold text-lg text-slate-800 dark:text-white mb-4">Agenda de Reservas</h3>
|
|
{facilityBookings.length === 0 ? (
|
|
<div className="text-center py-12 text-slate-400 dark:text-dark-mute border-2 border-dashed border-slate-200 dark:border-dark-border rounded-xl">
|
|
<Calendar size={48} className="mx-auto mb-2 opacity-20" />
|
|
<p>Sem reservas agendadas para este espaço.</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{facilityBookings.map(booking => (
|
|
<div key={booking.id} className="border border-slate-200 dark:border-dark-border rounded-lg p-4 hover:shadow-md transition-all bg-white dark:bg-dark-card relative overflow-hidden group">
|
|
<div className={`absolute top-0 left-0 w-1 h-full ${facilityType === 'gym' ? 'bg-blue-500' : facilityType === 'hall' ? 'bg-purple-500' : 'bg-green-500'}`}></div>
|
|
<div className="flex justify-between items-start mb-2 pl-2">
|
|
<span className="font-mono text-sm font-bold text-slate-500 dark:text-slate-400">{booking.date}</span>
|
|
<Badge status={booking.status} />
|
|
</div>
|
|
<h4 className="font-bold text-slate-800 dark:text-white text-lg pl-2">{booking.time}</h4>
|
|
<p className="text-sm text-slate-600 dark:text-dark-mute pl-2 mt-1 flex items-center gap-2">
|
|
<Users size={14} /> {booking.resident}
|
|
</p>
|
|
{booking.cost > 0 && (
|
|
<div className="mt-3 pl-2 pt-3 border-t border-slate-100 dark:border-dark-border text-right">
|
|
<span className="text-xs font-medium text-slate-400 uppercase">Custo:</span>
|
|
<span className="ml-2 font-bold text-slate-800 dark:text-white">{booking.cost}€</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const MapView = () => (
|
|
<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">
|
|
<div className="p-6 border-b border-slate-100 dark:border-dark-border flex justify-between items-center">
|
|
<div>
|
|
<h3 className="font-bold text-lg text-slate-800 dark:text-white">Mapa do Condomínio</h3>
|
|
<p className="text-sm text-slate-500 dark:text-dark-mute">Plantas e Localizações</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<span className="flex items-center gap-1 text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 px-2 py-1 rounded"><div className="w-2 h-2 rounded-full bg-blue-500"></div> Comum</span>
|
|
<span className="flex items-center gap-1 text-xs font-medium bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 px-2 py-1 rounded"><div className="w-2 h-2 rounded-full bg-orange-500"></div> Blocos</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 p-6 bg-slate-50 dark:bg-dark-bg overflow-auto flex items-center justify-center">
|
|
<div className="relative w-[800px] h-[500px] bg-white dark:bg-dark-surface border-2 border-slate-300 dark:border-dark-border rounded-xl shadow-xl p-8 transform hover:scale-[1.01] transition-transform duration-500">
|
|
<div className="absolute inset-0 opacity-5 bg-[linear-gradient(0deg,transparent_24%,#000_25%,#000_26%,transparent_27%,transparent_74%,#000_75%,#000_76%,transparent_77%,transparent),linear-gradient(90deg,transparent_24%,#000_25%,#000_26%,transparent_27%,transparent_74%,#000_75%,#000_76%,transparent_77%,transparent)] bg-[length:50px_50px]"></div>
|
|
|
|
<div className="absolute top-1/2 left-0 w-full h-16 bg-slate-200 dark:bg-dark-card transform -translate-y-1/2 border-y-2 border-slate-300 dark:border-slate-600 border-dashed flex items-center justify-center text-slate-400 font-bold tracking-[1em] opacity-50">VIA CENTRAL</div>
|
|
|
|
<div className="absolute top-10 left-10 w-40 h-40 bg-orange-100 dark:bg-orange-900/40 border-2 border-orange-300 dark:border-orange-700/50 rounded-lg flex flex-col items-center justify-center hover:bg-orange-200 dark:hover:bg-orange-900/60 cursor-pointer transition-colors group">
|
|
<Building2 size={32} className="text-orange-500 mb-2" />
|
|
<span className="font-bold text-orange-800 dark:text-orange-200">Bloco A</span>
|
|
<div className="absolute -bottom-8 opacity-0 group-hover:opacity-100 bg-black text-white text-xs px-2 py-1 rounded transition-opacity">10 andares • 20 Frações</div>
|
|
</div>
|
|
|
|
<div className="absolute bottom-10 left-10 w-40 h-40 bg-orange-100 dark:bg-orange-900/40 border-2 border-orange-300 dark:border-orange-700/50 rounded-lg flex flex-col items-center justify-center hover:bg-orange-200 dark:hover:bg-orange-900/60 cursor-pointer transition-colors group">
|
|
<Building2 size={32} className="text-orange-500 mb-2" />
|
|
<span className="font-bold text-orange-800 dark:text-orange-200">Bloco B</span>
|
|
<div className="absolute -top-8 opacity-0 group-hover:opacity-100 bg-black text-white text-xs px-2 py-1 rounded transition-opacity">8 andares • 16 Frações</div>
|
|
</div>
|
|
|
|
<div className="absolute top-10 right-10 w-64 h-48 bg-green-100 dark:bg-green-900/40 border-2 border-green-300 dark:border-green-700/50 rounded-2xl flex flex-col items-center justify-center hover:bg-green-200 dark:hover:bg-green-900/60 cursor-pointer transition-colors group">
|
|
<Trophy size={40} className="text-green-600 mb-2" />
|
|
<span className="font-bold text-green-800 dark:text-green-200">Parque de Jogos</span>
|
|
<span className="text-xs text-green-600 dark:text-green-300">Campo Polidesportivo</span>
|
|
<div className="absolute -bottom-8 opacity-0 group-hover:opacity-100 bg-black text-white text-xs px-2 py-1 rounded transition-opacity z-10">Clique para reservar</div>
|
|
</div>
|
|
|
|
<div className="absolute bottom-10 right-10 w-64 h-32 bg-blue-100 dark:bg-blue-900/40 border-2 border-blue-300 dark:border-blue-700/50 rounded-lg flex items-center justify-around hover:bg-blue-200 dark:hover:bg-blue-900/60 cursor-pointer transition-colors">
|
|
<div className="flex flex-col items-center group">
|
|
<PartyPopper size={24} className="text-blue-600 mb-1" />
|
|
<span className="text-xs font-bold text-blue-800 dark:text-blue-200">Salão Festas</span>
|
|
</div>
|
|
<div className="w-px h-20 bg-blue-300 dark:bg-blue-700"></div>
|
|
<div className="flex flex-col items-center group">
|
|
<Dumbbell size={24} className="text-blue-600 mb-1" />
|
|
<span className="text-xs font-bold text-blue-800 dark:text-blue-200">Ginásio</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-24 h-24 bg-slate-800 dark:bg-slate-700 rounded-full border-4 border-slate-200 dark:border-slate-600 shadow-xl flex flex-col items-center justify-center text-white z-10">
|
|
<Info size={24} />
|
|
<span className="text-[10px] mt-1 font-bold">Recepção</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const MaintenanceView = () => (
|
|
<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">
|
|
<div className="p-6 border-b border-slate-100 dark:border-dark-border flex justify-between items-center">
|
|
<div>
|
|
<h3 className="font-bold text-lg text-slate-800 dark:text-white">Manutenção e Ocorrências</h3>
|
|
<p className="text-sm text-slate-500 dark:text-dark-mute">Gestão de pedidos e reparações</p>
|
|
</div>
|
|
<button
|
|
onClick={() => handleOpenModal('issue')}
|
|
className="bg-orange-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-orange-700 flex items-center gap-2 shadow-sm transition-colors"
|
|
>
|
|
<Plus size={18} /> Reportar Problema
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-auto p-6 bg-slate-50 dark:bg-dark-bg">
|
|
{issues.length === 0 ? (
|
|
<div className="text-center py-12 text-slate-400 dark:text-dark-mute border-2 border-dashed border-slate-200 dark:border-dark-border rounded-xl">
|
|
<Wrench size={48} className="mx-auto mb-2 opacity-20" />
|
|
<p>Sem ocorrências registadas.</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{issues.map(issue => (
|
|
<div key={issue.id} className="bg-white dark:bg-dark-surface rounded-lg shadow-sm border border-slate-200 dark:border-dark-border p-5 hover:shadow-md transition-shadow relative overflow-hidden">
|
|
<div className={`absolute top-0 left-0 w-1 h-full ${issue.priority === 'Alta' ? 'bg-red-500' :
|
|
issue.priority === 'Média' ? 'bg-orange-500' : 'bg-blue-500'
|
|
}`}></div>
|
|
|
|
<div className="flex justify-between items-start mb-3 pl-2">
|
|
<Badge status={issue.status} />
|
|
<span className="text-xs text-slate-400 dark:text-slate-500 font-mono">{issue.date}</span>
|
|
</div>
|
|
|
|
<h4 className="font-bold text-slate-800 dark:text-white text-lg mb-1 pl-2">{issue.title}</h4>
|
|
<p className="text-sm text-slate-600 dark:text-dark-mute mb-4 pl-2 flex items-center gap-2">
|
|
<MapPin size={14} /> {issue.location}
|
|
</p>
|
|
|
|
<div className="pt-4 border-t border-slate-100 dark:border-dark-border flex justify-between items-center pl-2">
|
|
<span className={`text-xs font-bold px-2 py-1 rounded ${issue.priority === 'Alta' ? 'bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400' :
|
|
issue.priority === 'Média' ? 'bg-orange-50 dark:bg-orange-900/20 text-orange-700 dark:text-orange-400' : 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400'
|
|
}`}>
|
|
Prioridade {issue.priority}
|
|
</span>
|
|
|
|
{userRole === 'admin' && issue.status !== 'Resolvido' && (
|
|
<button
|
|
onClick={() => handleResolveIssue(issue.id)}
|
|
className="text-sm text-green-600 dark:text-green-400 font-medium hover:text-green-700 dark:hover:text-green-300 flex items-center gap-1"
|
|
>
|
|
<CheckCircle size={16} /> Resolver
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const ProfileView = ({ theme, setTheme }) => {
|
|
const [activeSection, setActiveSection] = useState('personal');
|
|
|
|
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">
|
|
<div className="flex flex-col md:flex-row h-full">
|
|
{/* Profile Sidebar */}
|
|
<div className="w-full md:w-64 bg-slate-50 dark:bg-dark-bg border-r border-slate-100 dark:border-dark-border p-6 flex flex-col gap-2">
|
|
<div className="text-center mb-6">
|
|
<div className="w-24 h-24 bg-blue-100 dark:bg-blue-900/30 rounded-full mx-auto flex items-center justify-center text-blue-600 dark:text-blue-400 font-bold text-3xl mb-3 border-4 border-white dark:border-dark-surface shadow-sm">
|
|
{userRole === 'admin' ? 'AD' : 'MO'}
|
|
</div>
|
|
<h3 className="font-bold text-slate-800 dark:text-white">{userRole === 'admin' ? 'Admin Condomínio' : 'Morador'}</h3>
|
|
<p className="text-xs text-slate-500 dark:text-dark-mute">{userRole === 'admin' ? 'Administrador Geral' : 'Residente'}</p>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => setActiveSection('personal')}
|
|
className={`text-left px-4 py-3 rounded-lg text-sm font-medium transition-colors flex items-center gap-3 ${activeSection === 'personal' ? 'bg-white dark:bg-dark-surface text-blue-600 dark:text-blue-400 shadow-sm' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-dark-card'}`}
|
|
>
|
|
<Users size={18} /> Dados Pessoais
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveSection('security')}
|
|
className={`text-left px-4 py-3 rounded-lg text-sm font-medium transition-colors flex items-center gap-3 ${activeSection === 'security' ? 'bg-white dark:bg-dark-surface text-blue-600 dark:text-blue-400 shadow-sm' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-dark-card'}`}
|
|
>
|
|
<LogOut size={18} className="rotate-90" /> Segurança
|
|
</button>
|
|
{userRole === 'admin' && (
|
|
<button
|
|
onClick={() => setActiveSection('permissions')}
|
|
className={`text-left px-4 py-3 rounded-lg text-sm font-medium transition-colors flex items-center gap-3 ${activeSection === 'permissions' ? 'bg-white dark:bg-dark-surface text-blue-600 dark:text-blue-400 shadow-sm' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-dark-card'}`}
|
|
>
|
|
<CheckCircle size={18} /> Permissões
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={() => setActiveSection('settings')}
|
|
className={`text-left px-4 py-3 rounded-lg text-sm font-medium transition-colors flex items-center gap-3 ${activeSection === 'settings' ? 'bg-white dark:bg-dark-surface text-blue-600 dark:text-blue-400 shadow-sm' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-dark-card'}`}
|
|
>
|
|
<Wrench size={18} /> Preferências
|
|
</button>
|
|
</div>
|
|
|
|
{/* Profile Content */}
|
|
<div className="flex-1 p-8 overflow-y-auto">
|
|
{activeSection === 'personal' && (
|
|
<div className="animate-fade-in max-w-2xl">
|
|
<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 />
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<InputGroup label="Email" value="admin@condomaster.pt" type="email" />
|
|
<InputGroup label="Telefone" value="+351 912 345 678" />
|
|
</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">
|
|
Guardar Alterações
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeSection === 'security' && (
|
|
<div className="animate-fade-in max-w-2xl">
|
|
<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">Segurança</h3>
|
|
<div className="bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-4 mb-6 flex items-start gap-3">
|
|
<AlertCircle className="text-orange-600 dark:text-orange-400 flex-shrink-0 mt-0.5" size={20} />
|
|
<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>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<InputGroup label="Palavra-passe Atual" type="password" placeholder="••••••••" />
|
|
<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" />
|
|
</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">
|
|
Atualizar Segurança
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeSection === 'permissions' && (
|
|
<div className="animate-fade-in max-w-2xl">
|
|
<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">Nível de Acesso</h3>
|
|
|
|
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4 mb-6 flex items-center gap-3">
|
|
<div className="bg-green-100 dark:bg-green-800/50 p-2 rounded-full">
|
|
<CheckCircle className="text-green-600 dark:text-green-400" size={24} />
|
|
</div>
|
|
<div>
|
|
<h4 className="font-bold text-green-800 dark:text-green-300">Acesso Total (Admin)</h4>
|
|
<p className="text-xs text-green-700 dark:text-green-400">Tem permissões totais para gerir condóminos, finanças e configurações.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<h4 className="text-sm font-bold text-slate-700 dark:text-slate-300 mb-2">Permissões Específicas:</h4>
|
|
{['Gerir Condóminos (Criar, Editar, Eliminar)', 'Gestão Financeira Completa', 'Moderação de Ocorrências', 'Configuração do Sistema', 'Gestão de Usuários'].map((perm, idx) => (
|
|
<div key={idx} className="flex items-center gap-3 p-3 border border-slate-100 dark:border-dark-border rounded-lg bg-slate-50 dark:bg-dark-card">
|
|
<div className="w-2 h-2 rounded-full bg-green-500"></div>
|
|
<span className="text-sm text-slate-600 dark:text-slate-300">{perm}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeSection === 'settings' && (
|
|
<div className="animate-fade-in max-w-2xl">
|
|
<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">Preferências da Aplicação</h3>
|
|
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h4 className="text-sm font-bold text-slate-700 dark:text-slate-300 mb-3">Notificações</h4>
|
|
<div className="space-y-2">
|
|
<label className="flex items-center gap-3 p-3 border border-slate-200 dark:border-dark-border rounded-lg cursor-pointer hover:bg-slate-50 dark:hover:bg-dark-card transition-colors">
|
|
<input type="checkbox" checked className="w-4 h-4 text-blue-600 rounded" readOnly />
|
|
<span className="text-sm text-slate-700 dark:text-slate-300">Alertas por Email</span>
|
|
</label>
|
|
<label className="flex items-center gap-3 p-3 border border-slate-200 dark:border-dark-border rounded-lg cursor-pointer hover:bg-slate-50 dark:hover:bg-dark-card transition-colors">
|
|
<input type="checkbox" checked className="w-4 h-4 text-blue-600 rounded" readOnly />
|
|
<span className="text-sm text-slate-700 dark:text-slate-300">Notificações Push no Navegador</span>
|
|
</label>
|
|
<label className="flex items-center gap-3 p-3 border border-slate-200 dark:border-dark-border rounded-lg cursor-pointer hover:bg-slate-50 dark:hover:bg-dark-card transition-colors">
|
|
<input type="checkbox" className="w-4 h-4 text-blue-600 rounded" readOnly />
|
|
<span className="text-sm text-slate-700 dark:text-slate-300">Relatórios Semanais Automáticos</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="text-sm font-bold text-slate-700 mb-3">Aparência</h4>
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div
|
|
onClick={() => setTheme('light')}
|
|
className={`border-2 ${theme === 'light' ? 'border-blue-500 bg-blue-50' : 'border-slate-200 bg-slate-50'} p-3 rounded-lg text-center cursor-pointer transition-colors`}
|
|
>
|
|
<div className="w-full h-12 bg-white border border-slate-200 rounded mb-2 shadow-sm"></div>
|
|
<span className={`text-xs font-bold ${theme === 'light' ? 'text-blue-600' : 'text-slate-500'}`}>Claro</span>
|
|
</div>
|
|
<div
|
|
onClick={() => setTheme('dark')}
|
|
className={`border-2 ${theme === 'dark' ? 'border-blue-500 bg-blue-50' : 'border-slate-200 bg-slate-50'} p-3 rounded-lg text-center cursor-pointer transition-colors`}
|
|
>
|
|
<div className="w-full h-12 bg-slate-800 rounded mb-2 shadow-sm border border-slate-700"></div>
|
|
<span className={`text-xs font-bold ${theme === 'dark' ? 'text-blue-600' : 'text-slate-500'}`}>Escuro</span>
|
|
</div>
|
|
<div
|
|
onClick={() => setTheme('system')}
|
|
className={`border-2 ${theme === 'system' ? 'border-blue-500 bg-blue-50' : 'border-slate-200 bg-slate-50'} p-3 rounded-lg text-center cursor-pointer transition-colors`}
|
|
>
|
|
<div className="w-full h-12 bg-gradient-to-r from-white to-slate-800 rounded mb-2 shadow-sm border border-slate-200"></div>
|
|
<span className={`text-xs font-bold ${theme === 'system' ? 'text-blue-600' : 'text-slate-500'}`}>Sistema</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
if (!isAuthenticated) {
|
|
return <LoginView onLogin={handleLogin} />;
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-screen bg-slate-50 dark:bg-dark-bg text-slate-800 dark:text-dark-text overflow-hidden font-sans transition-colors duration-300">
|
|
{/* Mobile Overlay */}
|
|
{isSidebarOpen && (
|
|
<div className="fixed inset-0 z-20 bg-black/50 lg:hidden" onClick={toggleSidebar}></div>
|
|
)}
|
|
|
|
{/* Sidebar */}
|
|
<aside className={`fixed inset-y-0 left-0 z-30 w-64 bg-white dark:bg-dark-surface border-r border-slate-200 dark:border-dark-border shadow-xl transform transition-transform duration-300 lg:translate-x-0 lg:static lg:shadow-none ${isSidebarOpen ? 'translate-x-0' : '-translate-x-full'}`}>
|
|
<div className="flex flex-col h-full">
|
|
<div className="p-6 flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-blue-600 rounded-lg text-white">
|
|
<Building2 size={24} />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-xl font-bold text-slate-800 dark:text-white">CondoMaster<span className="text-blue-600 dark:text-blue-400">Pro</span></h1>
|
|
<p className="text-xs text-slate-400 dark:text-dark-mute">Portal de Gestão</p>
|
|
</div>
|
|
</div>
|
|
<button onClick={toggleSidebar} className="lg:hidden text-slate-400"><X size={24} /></button>
|
|
</div>
|
|
|
|
<div className="flex-1 px-4 py-2 space-y-1 overflow-y-auto">
|
|
<div className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2 px-4 mt-2">Geral</div>
|
|
<SidebarItem icon={Building2} label="Dashboard" active={activeTab === 'dashboard'} onClick={() => { setActiveTab('dashboard'); setSidebarOpen(false); }} />
|
|
{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); }} />}
|
|
<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); }} />
|
|
|
|
<div className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2 px-4 mt-6">Espaços Comuns</div>
|
|
<SidebarItem icon={Dumbbell} label="Ginásio" active={activeTab === 'gym'} onClick={() => { setActiveTab('gym'); setSidebarOpen(false); }} />
|
|
<SidebarItem icon={PartyPopper} label="Salão de Festas" active={activeTab === 'hall'} onClick={() => { setActiveTab('hall'); setSidebarOpen(false); }} />
|
|
<SidebarItem icon={Trophy} label="Parque Jogos" active={activeTab === 'park'} onClick={() => { setActiveTab('park'); setSidebarOpen(false); }} />
|
|
</div>
|
|
|
|
<div className="p-4 border-t border-slate-100 dark:border-dark-border">
|
|
<button onClick={handleLogout} className="flex items-center gap-3 px-4 py-3 text-sm font-medium text-red-600 hover:bg-red-50 w-full rounded-lg transition-colors">
|
|
<LogOut size={20} />
|
|
<span>Terminar Sessão</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Main Content */}
|
|
<main className="flex-1 flex flex-col h-full overflow-hidden relative">
|
|
{/* Header */}
|
|
<header className="h-16 bg-white dark:bg-dark-surface border-b border-slate-200 dark:border-dark-border flex items-center justify-between px-6 z-10 shadow-sm transition-colors duration-300">
|
|
<div className="flex items-center gap-4">
|
|
<button onClick={toggleSidebar} className="lg:hidden text-slate-600 dark:text-slate-300 hover:text-blue-600 p-2 -ml-2 rounded-lg hover:bg-slate-100 dark:hover:bg-dark-card">
|
|
<Menu size={24} />
|
|
</button>
|
|
<h2 className="text-xl font-bold text-slate-800 dark:text-white capitalize">{
|
|
activeTab === 'dashboard' ? 'Visão Geral' :
|
|
activeTab === 'residents' ? 'Condóminos' :
|
|
activeTab === 'finance' ? 'Gestão Financeira' :
|
|
activeTab === 'billing' ? 'Faturação e Cobranças' :
|
|
activeTab === 'maintenance' ? 'Ocorrências e Manutenção' :
|
|
activeTab === 'messages' ? 'Mensagens e Fórum' :
|
|
activeTab === 'map' ? 'Mapa do Condomínio' :
|
|
activeTab === 'all_bookings' ? 'Todas as Reservas' :
|
|
activeTab === 'gym' ? 'Ginásio' :
|
|
activeTab === 'hall' ? 'Salão de Festas' :
|
|
activeTab === 'park' ? 'Parque de Jogos' :
|
|
activeTab === 'profile' ? 'O Meu Perfil' : activeTab
|
|
}</h2>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4">
|
|
<div className="relative hidden md:block">
|
|
<Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
|
|
<input
|
|
type="text"
|
|
placeholder="Pesquisar..."
|
|
className="pl-10 pr-4 py-2 border border-slate-200 dark:border-dark-border rounded-full text-sm focus:outline-none focus:ring-2 focus:ring-blue-100 focus:border-blue-400 w-64 bg-white dark:bg-dark-card dark:text-white dark:placeholder-slate-500"
|
|
/>
|
|
</div>
|
|
|
|
{/* Notifications */}
|
|
<div className="relative" ref={notificationRef}>
|
|
<button
|
|
className="p-2 text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-dark-card rounded-full relative transition-colors"
|
|
onClick={() => setNotificationsOpen(!isNotificationsOpen)}
|
|
>
|
|
<Bell size={20} />
|
|
{unreadNotifications > 0 && (
|
|
<span className="absolute top-1 right-1 w-2.5 h-2.5 bg-red-500 rounded-full ring-2 ring-white dark:ring-dark-surface"></span>
|
|
)}
|
|
</button>
|
|
|
|
{isNotificationsOpen && (
|
|
<div className="absolute right-0 mt-2 w-80 bg-white dark:bg-dark-surface rounded-xl shadow-xl border border-slate-100 dark:border-dark-border overflow-hidden animate-slide-up origin-top-right z-50">
|
|
<div className="p-4 border-b border-slate-100 dark:border-dark-border flex justify-between items-center">
|
|
<h3 className="font-bold text-slate-800 dark:text-white">Notificações</h3>
|
|
<button onClick={handleClearNotifications} className="text-xs text-blue-600 dark:text-blue-400 hover:underline">Limpar</button>
|
|
</div>
|
|
<div className="max-h-80 overflow-y-auto">
|
|
{notificationsList.length === 0 ? (
|
|
<div className="p-8 text-center text-slate-400 dark:text-dark-mute text-sm">Sem novas notificações</div>
|
|
) : (
|
|
notificationsList.map(notif => (
|
|
<div key={notif.id} className={`p-4 border-b border-slate-50 dark:border-dark-border hover:bg-slate-50 dark:hover:bg-dark-card ${!notif.read ? 'bg-blue-50/30 dark:bg-blue-900/10' : ''}`}>
|
|
<div className="flex gap-3">
|
|
<div className={`mt-1 w-2 h-2 rounded-full flex-shrink-0 ${notif.type === 'info' ? 'bg-blue-500' : notif.type === 'success' ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
|
<div>
|
|
<p className="text-sm text-slate-700 dark:text-slate-200 leading-tight">{notif.message}</p>
|
|
<p className="text-xs text-slate-400 dark:text-slate-500 mt-1">{notif.time}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div
|
|
className="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-700/50 flex items-center justify-center text-blue-700 dark:text-blue-300 font-bold text-sm cursor-pointer hover:bg-blue-200 dark:hover:bg-blue-900/50 transition-colors"
|
|
onClick={() => setActiveTab('profile')}
|
|
title="Meu Perfil"
|
|
>
|
|
{userRole === 'admin' ? 'AD' : 'MO'}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Content Body */}
|
|
<div className="flex-1 overflow-y-auto p-4 md:p-6 lg:p-8 bg-slate-50 dark:bg-dark-bg scroll-smooth transition-colors duration-300">
|
|
<div className="max-w-7xl mx-auto h-full">
|
|
|
|
{/* --- DASHBOARD --- */}
|
|
{activeTab === 'dashboard' && <DashboardView />}
|
|
|
|
{/* --- MAPA --- */}
|
|
{activeTab === 'map' && <MapView />}
|
|
|
|
{/* --- GINÁSIO --- */}
|
|
{activeTab === 'gym' && (
|
|
<BookingView
|
|
facilityType="gym"
|
|
title="Ginásio Privado"
|
|
icon={Dumbbell}
|
|
description="Equipamento cardio e musculação disponível para todos os residentes."
|
|
priceInfo="Gratuito"
|
|
color="bg-gradient-to-r from-blue-500 to-blue-600"
|
|
/>
|
|
)}
|
|
|
|
{/* --- SALÃO --- */}
|
|
{activeTab === 'hall' && (
|
|
<BookingView
|
|
facilityType="hall"
|
|
title="Salão de Festas"
|
|
icon={PartyPopper}
|
|
description="Espaço amplo para eventos, aniversários e reuniões de condomínio."
|
|
priceInfo="50€ / Evento"
|
|
color="bg-gradient-to-r from-purple-500 to-purple-600"
|
|
/>
|
|
)}
|
|
|
|
{/* --- PARQUE --- */}
|
|
{activeTab === 'park' && (
|
|
<BookingView
|
|
facilityType="park"
|
|
title="Parque de Jogos"
|
|
icon={Trophy}
|
|
description="Campo polidesportivo exterior (Futebol, Basquetebol e Ténis)."
|
|
priceInfo="10€ / Hora"
|
|
color="bg-gradient-to-r from-green-500 to-green-600"
|
|
/>
|
|
)}
|
|
|
|
{/* --- ALL BOOKINGS --- */}
|
|
{activeTab === 'all_bookings' && (
|
|
<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">
|
|
<div className="p-6 border-b border-slate-100 dark:border-dark-border flex justify-between items-center">
|
|
<div>
|
|
<h3 className="font-bold text-lg text-slate-800 dark:text-white">Histórico de Reservas</h3>
|
|
<p className="text-sm text-slate-500 dark:text-dark-mute">Lista completa de agendamentos em todos os espaços de lazer</p>
|
|
</div>
|
|
<button onClick={() => setActiveTab('map')} 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 transition-colors">
|
|
<Map size={18} /> Nova Reserva
|
|
</button>
|
|
</div>
|
|
<div className="overflow-auto flex-1 p-4 space-y-3">
|
|
{bookings.map(booking => (
|
|
<div key={booking.id} className="flex items-center justify-between p-4 border border-slate-100 dark:border-dark-border rounded-lg hover:bg-slate-50 dark:hover:bg-dark-card transition-colors shadow-sm">
|
|
<div className="flex items-center gap-4">
|
|
<div className={`p-3 rounded-lg ${booking.facility === 'gym' ? 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400' : booking.facility === 'hall' ? 'bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400' : 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400'}`}>
|
|
{booking.facility === 'gym' ? <Dumbbell size={24} /> : booking.facility === 'hall' ? <PartyPopper size={24} /> : <Trophy size={24} />}
|
|
</div>
|
|
<div>
|
|
<p className="font-bold text-base text-slate-800 dark:text-slate-200">{booking.facilityName}</p>
|
|
<p className="text-sm text-slate-600 dark:text-slate-400">{booking.date} • {booking.time}</p>
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-base font-medium text-slate-700 dark:text-slate-300">{booking.resident}</p>
|
|
<div className="mt-1">
|
|
<Badge status={booking.status} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{bookings.length === 0 && (
|
|
<div className="text-center py-12">
|
|
<Calendar size={48} className="mx-auto text-slate-300 dark:text-slate-600 mb-4" />
|
|
<h3 className="text-lg font-bold text-slate-700 dark:text-slate-300">Sem reservas</h3>
|
|
<p className="text-slate-500 dark:text-slate-400 mt-2">Ainda não existem agendamentos no condomínio.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* --- RESIDENTS --- */}
|
|
{activeTab === 'residents' && (
|
|
<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">
|
|
<div className="p-6 border-b border-slate-100 dark:border-dark-border flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div>
|
|
<h3 className="font-bold text-lg text-slate-800 dark:text-white">Gestão de Condóminos</h3>
|
|
<p className="text-sm text-slate-500 dark:text-dark-mute">Total: {residents.length} frações registadas</p>
|
|
</div>
|
|
<div className="flex flex-col sm:flex-row gap-3">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={18} />
|
|
<input
|
|
type="text"
|
|
placeholder="Procurar fração ou nome..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="pl-10 pr-4 py-2 border border-slate-200 dark:border-dark-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-full sm:w-64 bg-white dark:bg-dark-card dark:text-white dark:placeholder-slate-500 transition-colors"
|
|
/>
|
|
</div>
|
|
<button
|
|
onClick={() => handleOpenModal('resident')}
|
|
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 transition-colors shadow-sm"
|
|
>
|
|
<Plus size={18} /> <span className="hidden sm:inline">Adicionar</span>
|
|
</button>
|
|
</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 sticky top-0">
|
|
<tr>
|
|
<th className="px-6 py-4">Fração</th>
|
|
<th className="px-6 py-4">Proprietário</th>
|
|
<th className="px-6 py-4">Contacto</th>
|
|
<th className="px-6 py-4">Estado Quotas</th>
|
|
<th className="px-6 py-4 text-center">Acesso</th>
|
|
<th className="px-6 py-4 text-right">Em Dívida</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">
|
|
{filteredResidents.map((resident) => (
|
|
<tr key={resident.id} className="hover:bg-slate-50 dark:hover:bg-dark-bg transition-colors group">
|
|
<td className="px-6 py-4 font-bold text-slate-700 dark:text-slate-200">{resident.unit}</td>
|
|
<td className="px-6 py-4">
|
|
<div className="flex flex-col">
|
|
<span className="text-slate-900 dark:text-white font-medium">{resident.name}</span>
|
|
<span className="text-xs text-slate-500 dark:text-dark-mute">{resident.email}</span>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 text-slate-600 dark:text-dark-mute">{resident.contact}</td>
|
|
<td className="px-6 py-4"><Badge status={resident.status} /></td>
|
|
<td className="px-6 py-4 text-center">
|
|
<button
|
|
onClick={() => handleToggleRole(resident.id)}
|
|
className={`px-3 py-1 rounded-full text-xs font-bold transition-colors ${resident.role === 'admin' ? 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400 dark:hover:bg-purple-900/50' : 'bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700'}`}
|
|
title="Mudar Permissões"
|
|
>
|
|
{resident.role === 'admin' ? 'Admin' : 'Morador'}
|
|
</button>
|
|
</td>
|
|
<td className={`px-6 py-4 text-right font-medium ${resident.pending > 0 ? 'text-red-600 dark:text-red-400' : 'text-slate-600 dark:text-slate-400'}`}>
|
|
{Number(resident.pending).toFixed(2)}€
|
|
</td>
|
|
<td className="px-6 py-4 text-center">
|
|
<div className="flex items-center justify-center gap-2 opacity-100 sm:opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<button
|
|
onClick={() => handleOpenModal('resident', resident)}
|
|
className="p-1.5 text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/30 rounded-lg transition-colors" title="Editar"
|
|
>
|
|
<Edit2 size={16} />
|
|
</button>
|
|
<button
|
|
onClick={() => handleDeleteResident(resident.id)}
|
|
className="p-1.5 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors" title="Eliminar"
|
|
>
|
|
<Trash2 size={16} />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
<div id="formCondominio" style={{display: 'none'}}>
|
|
<input id="fracao" placeholder="Fração" />
|
|
<input id="proprietario" placeholder="Proprietário" />
|
|
<input id="contacto" placeholder="Contacto" />
|
|
<button id="guardar">Guardar</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* --- BILLING / COBRANÇAS --- */}
|
|
{activeTab === 'billing' && userRole === 'admin' && (
|
|
<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">
|
|
<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">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>
|
|
</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">Fração</th>
|
|
<th className="px-6 py-4">Condómino</th>
|
|
<th className="px-6 py-4">Quotas em Atraso</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">
|
|
{residents.map((resident) => (
|
|
<tr key={resident.id} className="hover:bg-slate-50 dark:hover:bg-dark-bg transition-colors">
|
|
<td className="px-6 py-4 font-bold text-slate-700 dark:text-slate-200">{resident.unit}</td>
|
|
<td className="px-6 py-4 text-slate-800 dark:text-white">{resident.name}</td>
|
|
<td className={`px-6 py-4 font-medium ${resident.pending > 0 ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-500'}`}>
|
|
{resident.pending > 0 ? `${Number(resident.pending).toFixed(2)}€` : 'Regularizado'}
|
|
</td>
|
|
<td className="px-6 py-4 text-center">
|
|
<button
|
|
onClick={() => showNotification(`Aviso de cobrança enviado para ${resident.email}`, 'success')}
|
|
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}
|
|
>
|
|
Notificar
|
|
</button>
|
|
<button
|
|
onClick={() => showNotification(`Fatura instantânea gerada para a fração ${resident.unit}`, 'success')}
|
|
className="ml-2 px-3 py-1.5 rounded-lg text-xs font-bold transition-colors shadow-sm bg-slate-800 text-white hover:bg-slate-900 dark:bg-slate-700 dark:hover:bg-slate-600"
|
|
>
|
|
Faturar na Hora
|
|
</button>
|
|
<button
|
|
onClick={() => showNotification(`Fatura enviada com sucesso para ${resident.email}`, 'success')}
|
|
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
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* --- FINANCES --- */}
|
|
{/* --- FINANCES --- */}
|
|
{activeTab === 'finance' && (
|
|
<div className="space-y-6 animate-fade-in h-full flex flex-col">
|
|
<div className="grid grid-cols-1 md:grid-cols-3 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-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">Receitas (Global)</h4>
|
|
<p className="text-2xl font-bold text-slate-800 dark:text-white mt-1">{totalIncome.toFixed(2)}€</p>
|
|
</div>
|
|
<TrendingUp className="absolute right-4 bottom-4 text-green-100 dark:text-green-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-red-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">Despesas (Global)</h4>
|
|
<p className="text-2xl font-bold text-slate-800 dark:text-white mt-1">{totalExpense.toFixed(2)}€</p>
|
|
</div>
|
|
<TrendingDown className="absolute right-4 bottom-4 text-red-100 dark:text-red-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-blue-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">Balanço Líquido</h4>
|
|
<p className={`text-2xl font-bold mt-1 ${balance >= 0 ? 'text-blue-600 dark:text-blue-400' : 'text-red-600 dark:text-red-400'}`}>
|
|
{balance > 0 ? '+' : ''}{balance.toFixed(2)}€
|
|
</p>
|
|
</div>
|
|
<Wallet className="absolute right-4 bottom-4 text-blue-100 dark:text-blue-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 justify-between items-center">
|
|
<div className="flex items-center gap-2">
|
|
<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>
|
|
<div className="overflow-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 sticky top-0 z-10">
|
|
<tr>
|
|
<th className="px-6 py-4">Data</th>
|
|
<th className="px-6 py-4">Categoria</th>
|
|
<th className="px-6 py-4">Descrição</th>
|
|
<th className="px-6 py-4">Tipo</th>
|
|
<th className="px-6 py-4 text-right">Valor</th>
|
|
<th className="px-6 py-4 text-center">Recibo</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-100 dark:divide-dark-border">
|
|
{finances.map((item) => (
|
|
<tr key={item.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 font-mono">{item.date}</td>
|
|
<td className="px-6 py-4 font-bold text-slate-700 dark:text-slate-200">{item.category}</td>
|
|
<td className="px-6 py-4 text-slate-500 dark:text-dark-mute">{item.desc}</td>
|
|
<td className="px-6 py-4">
|
|
<Badge status={item.type === 'income' ? 'Receita' : 'Despesa'} />
|
|
</td>
|
|
<td className={`px-6 py-4 text-right font-bold font-mono ${item.type === 'income' ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}>
|
|
{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">
|
|
<FileText size={16} />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* --- MESSAGES --- */}
|
|
{activeTab === 'messages' && (
|
|
<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 md:flex-row min-h-[500px]">
|
|
{/* Contact List */}
|
|
<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">
|
|
<Plus size={18} />
|
|
</button>
|
|
</div>
|
|
<div className="p-3 border-b border-slate-100 dark:border-dark-border">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={16} />
|
|
<input type="text" placeholder="Procurar mensagens..." className="w-full pl-9 pr-3 py-2 bg-slate-100 dark:bg-dark-card border-none rounded-lg text-sm focus:ring-2 focus:ring-blue-500 dark:text-white" />
|
|
</div>
|
|
</div>
|
|
<div className="flex-1 overflow-y-auto">
|
|
<div className="p-3 border-b-2 border-blue-100 dark:border-blue-900/30 bg-blue-50/50 dark:bg-blue-900/10 cursor-pointer hover:bg-slate-50 dark:hover:bg-dark-card transition-colors">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center text-blue-600 dark:text-blue-400 font-bold shrink-0">
|
|
<Users size={20} />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex justify-between items-baseline mb-0.5">
|
|
<h4 className="text-sm font-bold text-slate-800 dark:text-slate-200 truncate">Fórum do Condomínio</h4>
|
|
<span className="text-xs text-blue-600 dark:text-blue-400 font-medium whitespace-nowrap">Agora</span>
|
|
</div>
|
|
<p className="text-xs text-slate-500 dark:text-slate-400 truncate">Administração: Reunião na próxima sexta.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{residents.slice(0, 4).map(res => (
|
|
<div key={res.id} className="p-3 border-b border-slate-50 dark:border-dark-border/50 cursor-pointer hover:bg-slate-50 dark:hover:bg-dark-card transition-colors">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center text-slate-600 dark:text-slate-300 font-bold shrink-0 text-sm">
|
|
{res.name.substring(0, 2).toUpperCase()}
|
|
</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">{res.name} ({res.unit})</h4>
|
|
<span className="text-xs text-slate-400 whitespace-nowrap">Ontem</span>
|
|
</div>
|
|
<p className="text-xs text-slate-500 dark:text-slate-500 truncate">Tudo bem, tratamos disso!</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Chat Area */}
|
|
<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 bg-blue-100 dark:bg-blue-900 flex items-center justify-center text-blue-600 dark:text-blue-400 font-bold">
|
|
<Users size={20} />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-slate-800 dark:text-white">Fórum do Condomínio</h3>
|
|
<p className="text-xs text-green-500 font-medium">Todos os moradores</p>
|
|
</div>
|
|
</div>
|
|
<button className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200"><Info size={20}/></button>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
<div className="text-center my-4"><span className="text-xs bg-slate-200/50 dark:bg-slate-800 text-slate-500 px-3 py-1 rounded-full">Hoje</span></div>
|
|
|
|
<div className="flex justify-start">
|
|
<div className="bg-white dark:bg-dark-card border border-slate-100 dark:border-slate-800 text-slate-700 dark:text-slate-200 p-3 rounded-2xl rounded-tl-sm max-w-[80%] shadow-sm">
|
|
<p className="text-xs text-blue-600 dark:text-blue-400 font-bold mb-1">Administração</p>
|
|
<p className="text-sm">Bom dia a todos. Relembramos que a manutenção dos elevadores ocorrerá amanhã às 10h.</p>
|
|
<span className="text-[10px] text-slate-400 block text-right mt-1">09:00</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end">
|
|
<div className="bg-blue-600 text-white p-3 rounded-2xl rounded-tr-sm max-w-[80%] shadow-sm">
|
|
<p className="text-sm">Obrigado pelo aviso!</p>
|
|
<span className="text-[10px] text-blue-200 block text-right mt-1">09:15</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-start">
|
|
<div className="bg-white dark:bg-dark-card border border-slate-100 dark:border-slate-800 text-slate-700 dark:text-slate-200 p-3 rounded-2xl rounded-tl-sm max-w-[80%] shadow-sm">
|
|
<p className="text-xs text-orange-600 dark:text-orange-400 font-bold mb-1">Maria Pereira (2º Esq)</p>
|
|
<p className="text-sm">Alguém encontrou um porta-chaves com formato de gato na entrada do prédio?</p>
|
|
<span className="text-[10px] text-slate-400 block text-right mt-1">11:32</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-4 bg-white dark:bg-dark-surface border-t border-slate-100 dark:border-dark-border">
|
|
<form onSubmit={(e) => { e.preventDefault(); showNotification('A sua mensagem foi enviada!', 'success'); e.target.reset(); }} className="flex gap-2">
|
|
<button type="button" className="p-2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors hidden sm:block">
|
|
<Paperclip size={20} />
|
|
</button>
|
|
<input
|
|
type="text"
|
|
placeholder="Escreva a sua mensagem..."
|
|
className="flex-1 bg-slate-50 dark:bg-dark-bg border border-slate-200 dark:border-dark-border rounded-full px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:text-white"
|
|
/>
|
|
<button type="submit" className="p-2 bg-blue-600 text-white rounded-full hover:bg-blue-700 transition-colors shadow-sm flex items-center justify-center shrink-0 w-10 h-10">
|
|
<Send size={18} />
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* --- MAINTENANCE --- */}
|
|
{activeTab === 'maintenance' && <MaintenanceView />}
|
|
|
|
{/* --- PROFILE --- */}
|
|
{activeTab === 'profile' && <ProfileView theme={theme} setTheme={setTheme} />}
|
|
|
|
</div>
|
|
</div>
|
|
|
|
{/* --- Toast Notification --- */}
|
|
{notification && (
|
|
<div className={`fixed bottom-6 right-6 px-6 py-4 rounded-xl shadow-2xl flex items-center gap-3 animate-slide-up z-50 text-white ${notification.type === 'error' ? 'bg-red-600' : notification.type === 'warning' ? 'bg-orange-600' : 'bg-slate-800'}`}>
|
|
{notification.type === 'success' ? <CheckCircle size={20} className="text-green-400" /> : <AlertCircle size={20} />}
|
|
<span className="font-medium">{notification.message}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* --- Modals --- */}
|
|
|
|
{/* Resident Modal */}
|
|
<Modal isOpen={activeModal === 'resident'} onClose={handleCloseModal} title={editingItem ? "Editar Condómino" : "Novo Condómino"}>
|
|
<form onSubmit={handleSaveResident}>
|
|
<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="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} />
|
|
</div>
|
|
<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">
|
|
<Save size={20} /> Guardar Condómino
|
|
</button>
|
|
</form>
|
|
</Modal>
|
|
|
|
{/* Finance Modal */}
|
|
<Modal isOpen={activeModal === 'finance'} onClose={handleCloseModal} title="Registar Movimento Financeiro">
|
|
<form onSubmit={handleSaveFinance}>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<InputGroup label="Tipo" name="type" value={formData.type || 'expense'} onChange={handleInputChange} options={[{ value: 'income', label: 'Receita (+)' }, { value: 'expense', label: 'Despesa (-)' }]} />
|
|
<InputGroup label="Data" type="date" name="date" value={formData.date || ''} onChange={handleInputChange} required />
|
|
</div>
|
|
<InputGroup label="Categoria" name="category" value={formData.category || ''} onChange={handleInputChange} placeholder="Ex: Limpeza, Elevadores, Quotas..." required />
|
|
<InputGroup label="Valor (€)" type="number" name="amount" value={formData.amount || ''} onChange={handleInputChange} placeholder="0.00" required />
|
|
<InputGroup label="Descrição" name="desc" value={formData.desc || ''} onChange={handleInputChange} placeholder="Detalhes do movimento" />
|
|
<button type="submit" className="w-full bg-slate-800 hover:bg-slate-900 dark:bg-slate-700 dark:hover:bg-slate-600 text-white font-bold py-3 rounded-lg mt-4 flex justify-center gap-2 transition-colors">
|
|
<Save size={20} /> Registar Movimento
|
|
</button>
|
|
</form>
|
|
</Modal>
|
|
|
|
{/* Issue Modal */}
|
|
<Modal isOpen={activeModal === 'issue'} onClose={handleCloseModal} title="Reportar Ocorrência">
|
|
<form onSubmit={handleSaveIssue}>
|
|
<InputGroup label="Título do Problema" name="title" value={formData.title || ''} onChange={handleInputChange} placeholder="Ex: Lâmpada fundida" required />
|
|
<InputGroup label="Localização" name="location" value={formData.location || ''} onChange={handleInputChange} placeholder="Ex: Hall de entrada" required />
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<InputGroup label="Prioridade" name="priority" value={formData.priority || 'Média'} onChange={handleInputChange} options={[{ value: 'Baixa', label: 'Baixa' }, { value: 'Média', label: 'Média' }, { value: 'Alta', label: 'Alta' }]} />
|
|
<InputGroup label="Data" type="date" name="date" value={formData.date || ''} onChange={handleInputChange} />
|
|
</div>
|
|
<button type="submit" className="w-full bg-orange-600 hover:bg-orange-700 text-white font-bold py-3 rounded-lg mt-4 flex justify-center gap-2">
|
|
<Save size={20} /> Reportar
|
|
</button>
|
|
</form>
|
|
</Modal>
|
|
|
|
{/* Booking Modal */}
|
|
<Modal isOpen={activeModal === 'booking'} onClose={handleCloseModal} title="Nova Reserva">
|
|
<form onSubmit={handleSaveBooking}>
|
|
<InputGroup label="Espaço" name="facility" value={formData.facility || 'gym'} onChange={handleInputChange} options={[{ value: 'gym', label: 'Ginásio' }, { value: 'hall', label: 'Salão de Festas' }, { value: 'park', label: 'Parque de Jogos' }]} />
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<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 />
|
|
|
|
<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>
|
|
<span className="text-xl font-bold text-slate-800 dark:text-white">{formData.cost || 0}€</span>
|
|
</div>
|
|
|
|
<button type="submit" className="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 rounded-lg flex justify-center gap-2">
|
|
<CheckCircle size={20} /> Confirmar Reserva
|
|
</button>
|
|
</form>
|
|
</Modal>
|
|
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const root = createRoot(document.getElementById('root'));
|
|
root.render(<App />);
|
|
</script>
|
|
<!-- Firebase configs moved to top in React Module -->
|
|
|
|
</body>
|
|
</html> |