mapa e definições
This commit is contained in:
243
index.html
243
index.html
@@ -115,7 +115,7 @@
|
||||
TrendingUp, TrendingDown, CheckCircle, AlertCircle, Clock, LogOut,
|
||||
Edit2, Trash2, Save, Filter, MoreVertical, FileText,
|
||||
Dumbbell, PartyPopper, Trophy, Map, Calendar, MapPin, Info,
|
||||
MessageCircle, Paperclip, Send, Store, HeartPulse, Waves, ShoppingCart, Navigation, Car
|
||||
MessageCircle, Paperclip, Send, Store, HeartPulse, Waves, ShoppingCart, Navigation, Car, Home, Anchor, Fuel
|
||||
} from 'lucide-react';
|
||||
|
||||
import { app } from './firebase.js';
|
||||
@@ -533,7 +533,7 @@
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
if (window.confirm('Tem a certeza que deseja terminar sessão?')) {
|
||||
openConfirm('Tem a certeza que deseja terminar sessão?', () => {
|
||||
sessionStorage.removeItem('condo_auth');
|
||||
sessionStorage.removeItem('condo_role');
|
||||
sessionStorage.removeItem('condo_user_name');
|
||||
@@ -545,7 +545,7 @@
|
||||
setCurrentUserId('0');
|
||||
setUserStatus('aprovado');
|
||||
setActiveTab('dashboard');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const [residents, setResidents] = useState([]);
|
||||
@@ -558,6 +558,7 @@
|
||||
const [newMessageText, setNewMessageText] = useState('');
|
||||
const [activeChat, setActiveChat] = useState({ type: 'global', id: 'global', name: 'Fórum do Condomínio' });
|
||||
const [chatGroups, setChatGroups] = useState([]);
|
||||
const [adminProfile, setAdminProfile] = useState({});
|
||||
const [isCreateGroupModalOpen, setIsCreateGroupModalOpen] = useState(false);
|
||||
const [newGroupName, setNewGroupName] = useState('');
|
||||
const [newGroupMembers, setNewGroupMembers] = useState([]);
|
||||
@@ -593,6 +594,9 @@
|
||||
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);
|
||||
const unsubAdmin = onValue(ref(db, 'configuracoes/admin_profile'), (snapshot) => {
|
||||
if (snapshot.exists()) setAdminProfile(snapshot.val());
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubResidents();
|
||||
@@ -602,6 +606,7 @@
|
||||
unsubInvoices();
|
||||
unsubFaturas();
|
||||
unsubGroups();
|
||||
unsubAdmin();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -679,6 +684,9 @@
|
||||
const [activeModal, setActiveModal] = useState(null);
|
||||
const [editingItem, setEditingItem] = useState(null);
|
||||
|
||||
const [confirmDialog, setConfirmDialog] = useState({ isOpen: false, message: '', onConfirm: null });
|
||||
const openConfirm = (message, onConfirm) => setConfirmDialog({ isOpen: true, message, onConfirm });
|
||||
|
||||
const [notification, setNotification] = useState(null);
|
||||
|
||||
const notificationRef = useRef(null);
|
||||
@@ -864,7 +872,7 @@
|
||||
};
|
||||
|
||||
const handleDeleteResident = async (id) => {
|
||||
if (window.confirm('Tem a certeza que deseja eliminar este condómino?')) {
|
||||
openConfirm('Tem a certeza que deseja eliminar este condómino?', async () => {
|
||||
try {
|
||||
const residentRef = ref(db, `condominos/${id}`);
|
||||
await remove(residentRef);
|
||||
@@ -873,7 +881,7 @@
|
||||
console.error("Erro ao eliminar no Firebase:", error);
|
||||
showNotification("Erro ao eliminar.", "error");
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveFinance = async (e) => {
|
||||
@@ -1217,29 +1225,44 @@
|
||||
const [route, setRoute] = useState(null);
|
||||
const [isLocating, setIsLocating] = useState(false);
|
||||
|
||||
const IconMap = { Building2, ShoppingCart, Store, HeartPulse, Info, MapPin, Trophy, Dumbbell, PartyPopper, Waves };
|
||||
const IconMap = { Building2, ShoppingCart, Store, HeartPulse, Info, MapPin, Trophy, Dumbbell, PartyPopper, Waves, Home, Anchor, Fuel };
|
||||
|
||||
useEffect(() => {
|
||||
const defaultEspacos = {
|
||||
'bloco-a': { nome: 'Bloco A', tipo: 'Residencial', descricao: '10 andares • 20 Frações', icone: 'Building2', color: 'text-orange-500', bg: 'bg-orange-100 dark:bg-orange-900/40', border: 'border-orange-300 dark:border-orange-700/50', x: 8, y: 15, w: 20, h: 28, canBook: false, latitude: 38.7225, longitude: -9.1398 },
|
||||
'bloco-b': { nome: 'Bloco B', tipo: 'Residencial', descricao: '8 andares • 16 Frações', icone: 'Building2', color: 'text-orange-500', bg: 'bg-orange-100 dark:bg-orange-900/40', border: 'border-orange-300 dark:border-orange-700/50', x: 8, y: 55, w: 20, h: 28, canBook: false, latitude: 38.7215, longitude: -9.1398 },
|
||||
'mercado-1': { nome: 'Mini Mercado 1', tipo: 'Comércio', descricao: 'Bens de primeira necessidade', icone: 'ShoppingCart', color: 'text-amber-600', bg: 'bg-amber-100 dark:bg-amber-900/40', border: 'border-amber-300 dark:border-amber-700/50', x: 32, y: 20, w: 12, h: 15, canBook: false, latitude: 38.7228, longitude: -9.1390 },
|
||||
'mercado-2': { nome: 'Mini Mercado 2', tipo: 'Comércio', descricao: 'Mercearia e cafetaria', icone: 'Store', color: 'text-amber-600', bg: 'bg-amber-100 dark:bg-amber-900/40', border: 'border-amber-300 dark:border-amber-700/50', x: 32, y: 65, w: 12, h: 15, canBook: false, latitude: 38.7212, longitude: -9.1390 },
|
||||
'medico': { nome: 'Posto Médico', tipo: 'Serviços', descricao: 'Primeiros socorros e saúde', icone: 'HeartPulse', color: 'text-red-600', bg: 'bg-red-100 dark:bg-red-900/40', border: 'border-red-300 dark:border-red-700/50', x: 48, y: 20, w: 12, h: 15, canBook: false, latitude: 38.7228, longitude: -9.1385 },
|
||||
'reception': { nome: 'Recepção', tipo: 'Serviços', descricao: 'Segurança 24h e Encomendas', icone: 'Info', color: 'text-slate-700 dark:text-slate-300', bg: 'bg-slate-200 dark:bg-slate-700', border: 'border-slate-400 dark:border-slate-500', x: 48, y: 45, w: 8, h: 12, isRound: true, canBook: false, latitude: 38.7220, longitude: -9.1385 },
|
||||
'pool': { nome: 'Piscina', tipo: 'Lazer', descricao: 'Piscina exterior aquecida', icone: 'MapPin', color: 'text-teal-600', bg: 'bg-teal-100 dark:bg-teal-900/40', border: 'border-teal-300 dark:border-teal-700/50', x: 48, y: 65, w: 14, h: 18, canBook: false, latitude: 38.7212, longitude: -9.1385 },
|
||||
'park': { nome: 'Parque de Jogos', tipo: 'Lazer', descricao: 'Campo Polidesportivo', icone: 'Trophy', color: 'text-green-600', bg: 'bg-green-100 dark:bg-green-900/40', border: 'border-green-300 dark:border-green-700/50', x: 65, y: 15, w: 18, h: 25, canBook: true, bookId: 'park', latitude: 38.7225, longitude: -9.1375 },
|
||||
'gym': { nome: 'Ginásio', tipo: 'Lazer', descricao: 'Equipamento Cardio e Força', icone: 'Dumbbell', color: 'text-blue-600', bg: 'bg-blue-100 dark:bg-blue-900/40', border: 'border-blue-300 dark:border-blue-700/50', x: 65, y: 48, w: 14, h: 18, canBook: true, bookId: 'gym', latitude: 38.7218, longitude: -9.1375 },
|
||||
'hall': { nome: 'Salão Festas', type: 'Lazer', descricao: 'Capacidade 50 px', icone: 'PartyPopper', color: 'text-purple-600', bg: 'bg-purple-100 dark:bg-purple-900/40', border: 'border-purple-300 dark:border-purple-700/50', x: 65, y: 72, w: 14, h: 18, canBook: true, bookId: 'hall', latitude: 38.7210, longitude: -9.1375 },
|
||||
'deck': { nome: 'Deque do Rio', tipo: 'Lazer', descricao: 'Zona de relaxamento à beira rio', icone: 'Waves', color: 'text-cyan-700 dark:text-cyan-300', bg: 'bg-cyan-100 dark:bg-cyan-900/40', border: 'border-cyan-300 dark:border-cyan-700/50', x: 85, y: 40, w: 8, h: 30, canBook: false, latitude: 38.7220, longitude: -9.1360 },
|
||||
'bloco-a': { nome: 'Bloco A', tipo: 'Residencial', descricao: '10 andares • 20 Frações', icone: 'Building2', color: 'text-orange-500', bg: 'bg-orange-100 dark:bg-orange-900/40', border: 'border-orange-300 dark:border-orange-700/50', cx: 15, cy: 15, w: 20, h: 28, canBook: false, latitude: 38.7225, longitude: -9.1398 },
|
||||
'moradia-1': { nome: 'Moradia 1', tipo: 'Residencial', descricao: 'Moradia T3', icone: 'Home', color: 'text-teal-600', bg: 'bg-teal-100 dark:bg-teal-900/40', border: 'border-teal-300 dark:border-teal-700/50', cx: 12, cy: 40, w: 12, h: 14, canBook: false, latitude: 38.7220, longitude: -9.1396 },
|
||||
'moradia-2': { nome: 'Moradia 2', tipo: 'Residencial', descricao: 'Moradia T4', icone: 'Home', color: 'text-teal-600', bg: 'bg-teal-100 dark:bg-teal-900/40', border: 'border-teal-300 dark:border-teal-700/50', cx: 35, cy: 10, w: 12, h: 14, canBook: false, latitude: 38.7228, longitude: -9.1392 },
|
||||
'moradia-3': { nome: 'Moradia 3', tipo: 'Residencial', descricao: 'Moradia T3', icone: 'Home', color: 'text-teal-600', bg: 'bg-teal-100 dark:bg-teal-900/40', border: 'border-teal-300 dark:border-teal-700/50', cx: 45, cy: 35, w: 12, h: 14, canBook: false, latitude: 38.7222, longitude: -9.1388 },
|
||||
'moradia-4': { nome: 'Moradia 4', tipo: 'Residencial', descricao: 'Moradia T4', icone: 'Home', color: 'text-teal-600', bg: 'bg-teal-100 dark:bg-teal-900/40', border: 'border-teal-300 dark:border-teal-700/50', cx: 55, cy: 80, w: 12, h: 14, canBook: false, latitude: 38.7210, longitude: -9.1382 },
|
||||
'moradia-5': { nome: 'Moradia 5', tipo: 'Residencial', descricao: 'Moradia T3', icone: 'Home', color: 'text-teal-600', bg: 'bg-teal-100 dark:bg-teal-900/40', border: 'border-teal-300 dark:border-teal-700/50', cx: 25, cy: 60, w: 12, h: 14, canBook: false, latitude: 38.7214, longitude: -9.1394 },
|
||||
'moradia-6': { nome: 'Moradia 6', tipo: 'Residencial', descricao: 'Moradia T4', icone: 'Home', color: 'text-teal-600', bg: 'bg-teal-100 dark:bg-teal-900/40', border: 'border-teal-300 dark:border-teal-700/50', cx: 65, cy: 30, w: 12, h: 14, canBook: false, latitude: 38.7224, longitude: -9.1378 },
|
||||
'moradia-7': { nome: 'Moradia 7', tipo: 'Residencial', descricao: 'Moradia T3', icone: 'Home', color: 'text-teal-600', bg: 'bg-teal-100 dark:bg-teal-900/40', border: 'border-teal-300 dark:border-teal-700/50', cx: 8, cy: 80, w: 12, h: 14, canBook: false, latitude: 38.7216, longitude: -9.1399 },
|
||||
'moradia-8': { nome: 'Moradia 8', tipo: 'Residencial', descricao: 'Moradia T4', icone: 'Home', color: 'text-teal-600', bg: 'bg-teal-100 dark:bg-teal-900/40', border: 'border-teal-300 dark:border-teal-700/50', cx: 20, cy: 90, w: 12, h: 14, canBook: false, latitude: 38.7214, longitude: -9.1396 },
|
||||
'mercado-1': { nome: 'Supermercado', tipo: 'Comércio', descricao: 'Bens primeira necessidade', icone: 'ShoppingCart', color: 'text-amber-600', bg: 'bg-amber-100 dark:bg-amber-900/40', border: 'border-amber-300 dark:border-amber-700/50', cx: 40, cy: 50, w: 14, h: 16, canBook: false, latitude: 38.7218, longitude: -9.1389 },
|
||||
'mercado-2': { nome: 'Cafetaria', tipo: 'Comércio', descricao: 'Mercearia e cafetaria', icone: 'Store', color: 'text-amber-600', bg: 'bg-amber-100 dark:bg-amber-900/40', border: 'border-amber-300 dark:border-amber-700/50', cx: 75, cy: 65, w: 12, h: 14, canBook: false, latitude: 38.7213, longitude: -9.1372 },
|
||||
'medico': { nome: 'Clínica', tipo: 'Serviços', descricao: 'Saúde e Bem-estar', icone: 'HeartPulse', color: 'text-red-600', bg: 'bg-red-100 dark:bg-red-900/40', border: 'border-red-300 dark:border-red-700/50', cx: 35, cy: 90, w: 14, h: 16, canBook: false, latitude: 38.7208, longitude: -9.1392 },
|
||||
'reception': { nome: 'Portaria Principal', tipo: 'Serviços', descricao: 'Segurança 24h', icone: 'Info', color: 'text-slate-700 dark:text-slate-300', bg: 'bg-slate-200 dark:bg-slate-700', border: 'border-slate-400 dark:border-slate-500', cx: 8, cy: 50, w: 10, h: 10, isRound: true, canBook: false, latitude: 38.7219, longitude: -9.1400 },
|
||||
'pool': { nome: 'Piscina', tipo: 'Lazer', descricao: 'Piscina exterior', icone: 'MapPin', color: 'text-teal-600', bg: 'bg-teal-100 dark:bg-teal-900/40', border: 'border-teal-300 dark:border-teal-700/50', cx: 60, cy: 55, w: 16, h: 18, canBook: false, latitude: 38.7215, longitude: -9.1380 },
|
||||
'park': { nome: 'Parque', tipo: 'Lazer', descricao: 'Parque de jogos', icone: 'Trophy', color: 'text-green-600', bg: 'bg-green-100 dark:bg-green-900/40', border: 'border-green-300 dark:border-green-700/50', cx: 50, cy: 15, w: 18, h: 22, canBook: true, bookId: 'park', latitude: 38.7228, longitude: -9.1385 },
|
||||
'gym': { nome: 'Ginásio', tipo: 'Lazer', descricao: 'Cardio e Força', icone: 'Dumbbell', color: 'text-blue-600', bg: 'bg-blue-100 dark:bg-blue-900/40', border: 'border-blue-300 dark:border-blue-700/50', cx: 75, cy: 85, w: 14, h: 16, canBook: true, bookId: 'gym', latitude: 38.7209, longitude: -9.1374 },
|
||||
'hall': { nome: 'Salão Festas', type: 'Lazer', descricao: 'Capacidade 50 px', icone: 'PartyPopper', color: 'text-purple-600', bg: 'bg-purple-100 dark:bg-purple-900/40', border: 'border-purple-300 dark:border-purple-700/50', cx: 65, cy: 15, w: 14, h: 16, canBook: true, bookId: 'hall', latitude: 38.7205, longitude: -9.1398 },
|
||||
'marina': { nome: 'Aluguer Barcos', tipo: 'Náutica', descricao: 'Barcos e motas de água', icone: 'Anchor', color: 'text-cyan-700 dark:text-cyan-300', bg: 'bg-cyan-100 dark:bg-cyan-900/40', border: 'border-cyan-300 dark:border-cyan-700/50', cx: 86, cy: 35, w: 12, h: 14, canBook: true, bookId: 'marina', latitude: 38.7222, longitude: -9.1355 },
|
||||
'fuel': { nome: 'Bomba Náutica', tipo: 'Náutica', descricao: 'Abastecimento', icone: 'Fuel', color: 'text-red-500', bg: 'bg-red-100 dark:bg-red-900/40', border: 'border-red-300 dark:border-red-700/50', cx: 86, cy: 50, w: 10, h: 10, isRound: true, canBook: false, latitude: 38.7219, longitude: -9.1355 },
|
||||
'deck': { nome: 'Deque', tipo: 'Lazer', descricao: 'Lazer', icone: 'Waves', color: 'text-cyan-700 dark:text-cyan-300', bg: 'bg-cyan-100 dark:bg-cyan-900/40', border: 'border-cyan-300 dark:border-cyan-700/50', cx: 88, cy: 75, w: 10, h: 25, canBook: false, latitude: 38.7212, longitude: -9.1350 },
|
||||
};
|
||||
|
||||
const espacosRef = ref(db, 'espacos');
|
||||
const unsub = onValue(espacosRef, (snapshot) => {
|
||||
if (snapshot.exists()) {
|
||||
const data = snapshot.val();
|
||||
const loadedEspacos = Object.keys(data).map(key => ({ id: key, ...data[key] }));
|
||||
setEspacos(loadedEspacos);
|
||||
if (!data['moradia-8']) {
|
||||
set(ref(db, 'espacos'), defaultEspacos).catch(console.error);
|
||||
const loadedEspacos = Object.keys(defaultEspacos).map(key => ({ id: key, ...defaultEspacos[key] }));
|
||||
setEspacos(loadedEspacos);
|
||||
} else {
|
||||
const loadedEspacos = Object.keys(data).map(key => ({ id: key, ...data[key] }));
|
||||
setEspacos(loadedEspacos);
|
||||
}
|
||||
} else {
|
||||
// Seed inicial da base de dados se estiver vazia
|
||||
set(ref(db, 'espacos'), defaultEspacos).catch(console.error);
|
||||
@@ -1297,12 +1320,12 @@
|
||||
},
|
||||
(error) => {
|
||||
console.error("Erro de geolocalização:", error);
|
||||
alert("Não foi possível obter a sua localização. Verifique as permissões do browser.");
|
||||
showNotification("Não foi possível obter a sua localização. Verifique as permissões do browser.", "error");
|
||||
setIsLocating(false);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
alert("Geolocalização não é suportada por este browser.");
|
||||
showNotification("Geolocalização não é suportada por este browser.", "error");
|
||||
setIsLocating(false);
|
||||
}
|
||||
};
|
||||
@@ -1322,19 +1345,72 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row flex-1 min-h-[500px]">
|
||||
<div className="flex flex-col lg:flex-row flex-1 min-h-[700px]">
|
||||
{/* Área do Mapa */}
|
||||
<div className="flex-1 p-6 bg-slate-50 dark:bg-dark-bg flex items-center justify-center relative overflow-hidden">
|
||||
<div className="relative w-full max-w-[800px] aspect-[16/10] bg-white dark:bg-dark-surface border-2 border-slate-300 dark:border-dark-border rounded-xl shadow-xl p-4 transition-all duration-500 overflow-hidden">
|
||||
<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:40px_40px]"></div>
|
||||
<div className="relative w-full h-full max-h-[800px] min-h-[600px] bg-[#eef8f2] dark:bg-[#1a2e23] border-4 border-slate-300 dark:border-dark-border rounded-2xl shadow-2xl p-4 transition-all duration-500 overflow-hidden">
|
||||
<div className="absolute inset-0 opacity-10 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:40px_40px]"></div>
|
||||
|
||||
{/* Visual River */}
|
||||
<div className="absolute right-0 top-0 bottom-0 w-[12%] bg-blue-400/20 dark:bg-blue-600/20 border-l-4 border-blue-300/30 dark:border-blue-700/30 flex items-center justify-center overflow-hidden pointer-events-none">
|
||||
<div className="text-blue-500/30 dark:text-blue-400/20 font-bold text-3xl rotate-90 whitespace-nowrap tracking-[1em]">RIO</div>
|
||||
<div className="absolute right-0 top-0 bottom-0 w-[12%] bg-blue-400/30 dark:bg-blue-600/30 border-l-8 border-blue-300/50 dark:border-blue-700/50 flex items-center justify-center overflow-hidden pointer-events-none">
|
||||
<div className="text-blue-600/40 dark:text-blue-300/30 font-black text-4xl rotate-90 whitespace-nowrap tracking-[1em]">RIO TEJO</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-1/2 left-0 w-[88%] h-12 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-[0.5em] opacity-40 text-xs sm:text-sm pointer-events-none">VIA CENTRAL</div>
|
||||
{/* Ruas e Caminhos em SVG */}
|
||||
<svg viewBox="0 0 100 100" preserveAspectRatio="none" className="absolute inset-0 w-full h-full pointer-events-none z-0" style={{ overflow: 'visible' }}>
|
||||
{/* Via Central */}
|
||||
<rect x="0" y="46" width="88" height="8" fill="#cbd5e1" className="dark:fill-slate-700" />
|
||||
<line x1="0" y1="50" x2="88" y2="50" stroke="#f8fafc" strokeWidth="0.5" strokeDasharray="2,2" className="dark:stroke-slate-400" />
|
||||
|
||||
{/* Caminhos Secundários Orgânicos */}
|
||||
<g stroke="#cbd5e1" strokeWidth="3" className="dark:stroke-slate-700" strokeLinecap="round" strokeLinejoin="round" fill="none">
|
||||
{/* Top Arch */}
|
||||
<path d="M 25 50 C 25 15, 40 5, 55 5 C 75 5, 80 20, 80 30 C 80 40, 85 45, 85 50" />
|
||||
{/* Middle Top Winding */}
|
||||
<path d="M 40 50 C 40 30, 50 25, 60 25 C 70 25, 75 30, 80 30" />
|
||||
{/* Bottom Winding S-shape */}
|
||||
<path d="M 18 50 C 18 85, 45 85, 45 65 C 45 45, 65 95, 85 50" />
|
||||
|
||||
{/* Ramos de Ligação (Atalhos) */}
|
||||
<path d="M 15 15 L 28 15" strokeWidth="1.5" /> {/* Bloco A */}
|
||||
<path d="M 8 80 L 27 80" strokeWidth="1.5" /> {/* Moradia 7 */}
|
||||
<path d="M 20 90 L 20 60" strokeWidth="1.5" /> {/* Moradia 8 */}
|
||||
<path d="M 12 40 L 12 50" strokeWidth="1.5" /> {/* Moradia 1 */}
|
||||
<path d="M 35 10 L 35 15" strokeWidth="1.5" /> {/* Moradia 2 */}
|
||||
<path d="M 45 35 L 45 29" strokeWidth="1.5" /> {/* Moradia 3 */}
|
||||
<path d="M 55 80 L 55 64" strokeWidth="1.5" /> {/* Moradia 4 */}
|
||||
<path d="M 25 60 L 25 78" strokeWidth="1.5" /> {/* Moradia 5 */}
|
||||
<path d="M 65 30 L 65 26" strokeWidth="1.5" /> {/* Moradia 6 */}
|
||||
<path d="M 75 65 L 75 68" strokeWidth="1.5" /> {/* Cafetaria */}
|
||||
<path d="M 35 90 L 35 72" strokeWidth="1.5" /> {/* Clinica */}
|
||||
<path d="M 60 55 L 60 68" strokeWidth="1.5" /> {/* Piscina */}
|
||||
<path d="M 50 15 L 50 5" strokeWidth="1.5" /> {/* Parque */}
|
||||
<path d="M 75 85 L 72 65" strokeWidth="1.5" /> {/* Ginasio */}
|
||||
<path d="M 65 15 L 65 28" strokeWidth="1.5" /> {/* Salao */}
|
||||
<path d="M 86 35 L 80 35" strokeWidth="1.5" /> {/* Barcos */}
|
||||
<path d="M 88 75 L 78 60" strokeWidth="1.5" /> {/* Deque */}
|
||||
</g>
|
||||
<g stroke="#f8fafc" strokeWidth="0.5" strokeDasharray="1,1" className="dark:stroke-slate-400" strokeLinecap="round" strokeLinejoin="round" fill="none">
|
||||
{/* Centros das Vias Orgânicas */}
|
||||
<path d="M 25 50 C 25 15, 40 5, 55 5 C 75 5, 80 20, 80 30 C 80 40, 85 45, 85 50" />
|
||||
<path d="M 40 50 C 40 30, 50 25, 60 25 C 70 25, 75 30, 80 30" />
|
||||
<path d="M 18 50 C 18 85, 45 85, 45 65 C 45 45, 65 95, 85 50" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<div className="absolute top-[50%] left-0 w-[88%] h-12 transform -translate-y-1/2 flex items-center justify-center text-slate-500 dark:text-slate-300 font-black tracking-[0.8em] opacity-60 text-xs sm:text-sm pointer-events-none z-0">VIA CENTRAL</div>
|
||||
|
||||
{/* Árvores Decorativas (Espalhadas) */}
|
||||
{[
|
||||
{x: 5, y: 5}, {x: 12, y: 8}, {x: 25, y: 5}, {x: 45, y: 8}, {x: 60, y: 5}, {x: 80, y: 8},
|
||||
{x: 5, y: 90}, {x: 12, y: 85}, {x: 25, y: 90}, {x: 45, y: 88}, {x: 60, y: 92}, {x: 80, y: 85},
|
||||
{x: 25, y: 35}, {x: 45, y: 45}, {x: 80, y: 40},
|
||||
{x: 25, y: 65}, {x: 45, y: 60}, {x: 80, y: 60},
|
||||
{x: 50, y: 30}, {x: 55, y: 65}, {x: 10, y: 70}
|
||||
].map((tree, i) => (
|
||||
<div key={`tree-${i}`} className="absolute w-6 h-6 bg-green-600/50 dark:bg-green-800/50 rounded-full blur-[2px] shadow-lg pointer-events-none z-0" style={{ left: `${tree.x}%`, top: `${tree.y}%` }}></div>
|
||||
))}
|
||||
|
||||
{/* SVG Route Overlay */}
|
||||
{route && route.targetId && (
|
||||
<svg className="absolute inset-0 w-full h-full pointer-events-none z-30" style={{ overflow: 'visible' }}>
|
||||
@@ -1343,18 +1419,22 @@
|
||||
<polygon points="0 0, 10 3.5, 0 7" fill="#3b82f6" />
|
||||
</marker>
|
||||
</defs>
|
||||
{espacos.filter(e => e.id === route.targetId).map(target => (
|
||||
<path
|
||||
key="route-path"
|
||||
d={`M ${route.userX}% ${route.userY}% Q 50% 50% ${target.x + (target.w||0)/2}% ${target.y + (target.h||0)/2}%`}
|
||||
fill="none"
|
||||
stroke="#3b82f6"
|
||||
strokeWidth="4"
|
||||
strokeDasharray="8, 8"
|
||||
className="animate-[dash_1s_linear_infinite]"
|
||||
markerEnd="url(#arrowhead)"
|
||||
/>
|
||||
))}
|
||||
{espacos.filter(e => e.id === route.targetId).map(target => {
|
||||
const targetCX = target.cx !== undefined ? target.cx : target.x + (target.w||10) / 2;
|
||||
const targetCY = target.cy !== undefined ? target.cy : target.y + (target.h||10) / 2;
|
||||
return (
|
||||
<path
|
||||
key="route-path"
|
||||
d={`M ${route.userX}% ${route.userY}% Q 50% 50% ${targetCX}% ${targetCY}%`}
|
||||
fill="none"
|
||||
stroke="#3b82f6"
|
||||
strokeWidth="4"
|
||||
strokeDasharray="8, 8"
|
||||
className="animate-[dash_1s_linear_infinite]"
|
||||
markerEnd="url(#arrowhead)"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<circle cx={`${route.userX}%`} cy={`${route.userY}%`} r="6" fill="#3b82f6" className="animate-ping" />
|
||||
<circle cx={`${route.userX}%`} cy={`${route.userY}%`} r="4" fill="#1e40af" />
|
||||
<style>{`
|
||||
@@ -1365,17 +1445,26 @@
|
||||
|
||||
{espacos.map(loc => {
|
||||
const IconComp = IconMap[loc.icone] || MapPin;
|
||||
// Reduzir o tamanho em 25% (scale = 0.75) mantendo o centro
|
||||
const scale = 0.75;
|
||||
const w = (loc.w || 10) * scale;
|
||||
const h = (loc.h || 10) * scale;
|
||||
const cx = loc.cx !== undefined ? loc.cx : loc.x + (loc.w || 10) / 2;
|
||||
const cy = loc.cy !== undefined ? loc.cy : loc.y + (loc.h || 10) / 2;
|
||||
const x = cx - w / 2;
|
||||
const y = cy - h / 2;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={loc.id}
|
||||
onClick={() => setActivePoint(loc.id)}
|
||||
onMouseEnter={() => setActivePoint(loc.id)}
|
||||
className={`absolute flex flex-col items-center justify-center cursor-pointer transition-all duration-300 border-2 ${loc.bg} ${loc.border} ${activePoint === loc.id ? 'scale-110 shadow-lg z-20 ring-4 ring-blue-400/30' : 'hover:scale-105 shadow-sm z-10'} ${loc.isRound ? 'rounded-full' : 'rounded-lg'}`}
|
||||
style={{ left: `${loc.x}%`, top: `${loc.y}%`, width: `${loc.w}%`, height: `${loc.h}%` }}
|
||||
className={`absolute flex flex-col items-center justify-center cursor-pointer transition-all duration-300 border-2 ${loc.bg} ${loc.border} ${activePoint === loc.id ? 'scale-110 shadow-2xl z-20 ring-4 ring-blue-400/50' : 'hover:scale-105 shadow-md z-10'} ${loc.isRound ? 'rounded-full' : 'rounded-xl bg-white/90 dark:bg-dark-surface/90 backdrop-blur-sm'}`}
|
||||
style={{ left: `${x}%`, top: `${y}%`, width: `${w}%`, height: `${h}%` }}
|
||||
>
|
||||
<IconComp size={loc.isRound ? 16 : 24} className={`${loc.color} ${activePoint === loc.id ? 'animate-bounce' : ''}`} />
|
||||
<IconComp size={loc.isRound ? 14 : 20} className={`${loc.color} ${activePoint === loc.id ? 'animate-bounce' : ''}`} />
|
||||
{!loc.isRound && (
|
||||
<span className={`font-bold text-[9px] sm:text-[10px] mt-1 text-center px-1 text-slate-800 dark:text-slate-200 leading-tight`}>
|
||||
<span className={`font-black text-[9px] sm:text-[10px] mt-1 text-center px-1 text-slate-800 dark:text-slate-200 leading-tight drop-shadow-sm`}>
|
||||
{loc.nome}
|
||||
</span>
|
||||
)}
|
||||
@@ -1554,7 +1643,8 @@
|
||||
role: `Fração ${currentUserData.unit || 'N/A'}`,
|
||||
email: currentUserData.email || '',
|
||||
contact: currentUserData.contact || '',
|
||||
address: 'Morada do Condomínio'
|
||||
address: 'Morada do Condomínio',
|
||||
photoUrl: currentUserData.photoUrl || ''
|
||||
});
|
||||
} else {
|
||||
const adminRef = ref(db, 'configuracoes/admin_profile');
|
||||
@@ -1623,6 +1713,9 @@
|
||||
try {
|
||||
await set(ref(db, `condominos/${currentUserData.id}/email`), formData.email);
|
||||
await set(ref(db, `condominos/${currentUserData.id}/contact`), formData.contact);
|
||||
if (formData.photoUrl !== undefined) {
|
||||
await set(ref(db, `condominos/${currentUserData.id}/photoUrl`), formData.photoUrl);
|
||||
}
|
||||
showNotification('Dados atualizados com sucesso!', 'success');
|
||||
sendSystemNotification('Um utilizador atualizou os seus dados pessoais.', 'info', 'admin');
|
||||
} catch (error) {
|
||||
@@ -1641,15 +1734,44 @@
|
||||
}
|
||||
};
|
||||
|
||||
const fileInputRef = React.useRef(null);
|
||||
const handleImageChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
handleChange('photoUrl', reader.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
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 cursor-pointer relative group overflow-hidden transition-all"
|
||||
onClick={() => fileInputRef.current && fileInputRef.current.click()}
|
||||
>
|
||||
{formData.photoUrl ? (
|
||||
<img src={formData.photoUrl} alt="Perfil" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
userRole === 'admin' ? 'AD' : 'MO'
|
||||
)}
|
||||
<div className="absolute inset-0 bg-black/50 hidden group-hover:flex items-center justify-center text-white transition-all">
|
||||
<span className="text-xs font-medium">Alterar</span>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleImageChange}
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
/>
|
||||
<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>
|
||||
@@ -1997,7 +2119,12 @@
|
||||
onClick={() => setActiveTab('profile')}
|
||||
title="Meu Perfil"
|
||||
>
|
||||
{userRole === 'admin' ? 'AD' : 'MO'}
|
||||
{(() => {
|
||||
const currentUser = residents.find(r => r.id === currentUserId);
|
||||
const photoUrl = userRole === 'admin' ? adminProfile?.photoUrl : currentUser?.photoUrl;
|
||||
if (photoUrl) return <img src={photoUrl} alt="Perfil" className="w-full h-full rounded-full object-cover border border-slate-200 dark:border-slate-700" />;
|
||||
return userRole === 'admin' ? 'AD' : 'MO';
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -2803,6 +2930,28 @@
|
||||
</button>
|
||||
</form>
|
||||
</Modal>
|
||||
<Modal isOpen={confirmDialog.isOpen} onClose={() => setConfirmDialog(prev => ({ ...prev, isOpen: false }))} title="Confirmação">
|
||||
<div className="p-2">
|
||||
<p className="text-slate-700 dark:text-slate-300 text-base mb-6 text-center font-medium">{confirmDialog.message}</p>
|
||||
<div className="flex gap-3 justify-center">
|
||||
<button
|
||||
className="px-6 py-2 bg-slate-200 dark:bg-slate-700 hover:bg-slate-300 dark:hover:bg-slate-600 text-slate-800 dark:text-white rounded-lg transition-colors font-medium"
|
||||
onClick={() => setConfirmDialog(prev => ({ ...prev, isOpen: false }))}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
className="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors font-medium shadow-sm"
|
||||
onClick={() => {
|
||||
setConfirmDialog(prev => ({ ...prev, isOpen: false }));
|
||||
if (confirmDialog.onConfirm) confirmDialog.onConfirm();
|
||||
}}
|
||||
>
|
||||
Confirmar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user