Files
GestorCondominio/script.js
2026-03-18 10:37:10 +00:00

493 lines
18 KiB
JavaScript

const SUPABASE_URL = 'YOUR_SUPABASE_URL';
const SUPABASE_KEY = 'YOUR_SUPABASE_KEY';
const IS_MOCK = SUPABASE_URL.includes('YOUR_SUPABASE');
let supabase;
if (!IS_MOCK) {
supabase = supabase.createClient(SUPABASE_URL, SUPABASE_KEY);
} else {
console.warn("⚠️ MOCK MODE ATIVADO: Usando LocalStorage. Configure o Supabase para persistência real.");
showToast("Modo Demo Ativado (LocalStorage)", "warning");
}
document.addEventListener('DOMContentLoaded', () => {
checkLogin();
setupEventListeners();
});
function setupEventListeners() {
document.getElementById('login-form').addEventListener('submit', handleLogin);
document.getElementById('form-morador').addEventListener('submit', saveMorador);
document.getElementById('form-transacao').addEventListener('submit', saveTransacao);
document.getElementById('form-ocorrencia').addEventListener('submit', saveOcorrencia);
document.getElementById('form-aviso').addEventListener('submit', saveAviso);
if (sessionStorage.getItem('condoProUser')) {
renderDashboard();
}
}
function checkLogin() {
const user = sessionStorage.getItem('condoProUser');
if (user) {
document.getElementById('login-section').classList.add('d-none');
document.getElementById('app-layout').classList.remove('d-none');
document.getElementById('app-layout').classList.add('d-flex');
} else {
document.getElementById('login-section').classList.remove('d-none');
document.getElementById('app-layout').classList.add('d-none');
document.getElementById('app-layout').classList.remove('d-flex');
}
}
function handleLogin(e) {
e.preventDefault();
const pass = document.getElementById('login-password').value;
if (pass === 'admin123') {
sessionStorage.setItem('condoProUser', 'admin');
checkLogin();
renderDashboard();
showToast("Bem-vindo ao CondoPro!", "success");
} else {
document.getElementById('login-error').classList.remove('d-none');
showToast("Senha incorreta!", "danger");
}
}
function logout() {
sessionStorage.removeItem('condoProUser');
checkLogin();
showToast("Logout realizado.", "info");
}
function showToast(message, type = 'primary') {
const container = document.getElementById('toast-container');
const toastEl = document.createElement('div');
toastEl.className = `toast align-items-center text-white bg-${type} border-0`;
toastEl.setAttribute('role', 'alert');
toastEl.setAttribute('aria-live', 'assertive');
toastEl.setAttribute('aria-atomic', 'true');
toastEl.innerHTML = `
<div class="d-flex">
<div class="toast-body">
${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
container.appendChild(toastEl);
const toast = new bootstrap.Toast(toastEl, { delay: 3000 });
toast.show();
toastEl.addEventListener('hidden.bs.toast', () => {
toastEl.remove();
});
}
async function dbSelect(table, orderBy = null) {
if (IS_MOCK) {
const data = JSON.parse(localStorage.getItem(`condopro_${table}`)) || [];
if (orderBy) {
data.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
}
return { data, error: null };
}
let query = supabase.from(table).select('*');
if (orderBy) query = query.order(orderBy.col, { ascending: orderBy.asc });
return await query;
}
async function dbInsert(table, row) {
row.created_at = new Date().toISOString();
if (!row.id) row.id = Date.now();
if (IS_MOCK) {
const data = JSON.parse(localStorage.getItem(`condopro_${table}`)) || [];
data.push(row);
localStorage.setItem(`condopro_${table}`, JSON.stringify(data));
return { data: [row], error: null };
}
return await supabase.from(table).insert([row]);
}
async function dbDelete(table, id) {
if (IS_MOCK) {
let data = JSON.parse(localStorage.getItem(`condopro_${table}`)) || [];
data = data.filter(item => item.id != id); // Loose comparison for ID
localStorage.setItem(`condopro_${table}`, JSON.stringify(data));
return { error: null };
}
return await supabase.from(table).delete().eq('id', id);
}
function navigateTo(viewId) {
document.querySelectorAll('.nav-link').forEach(link => link.classList.remove('active'));
event.currentTarget.classList.add('active');
document.querySelectorAll('.content-view').forEach(view => view.classList.add('d-none'));
document.getElementById(`view-${viewId}`).classList.remove('d-none');
if (viewId === 'dashboard') renderDashboard();
if (viewId === 'moradores') renderMoradores();
if (viewId === 'financeiro') renderFinanceiro();
if (viewId === 'ocorrencias') renderOcorrencias();
if (viewId === 'avisos') renderAvisos();
document.getElementById('sidebar').classList.remove('show');
}
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('show');
}
let financeChartInstance = null;
async function renderDashboard() {
const { data: financeiro } = await dbSelect('financeiro');
const { data: moradores } = await dbSelect('moradores');
const { data: ocorrencias } = await dbSelect('ocorrencias');
let saldoTotal = 0;
let receitasMes = 0;
let despesasMes = 0;
if (financeiro) {
financeiro.forEach(t => {
const val = parseFloat(t.valor);
if (t.tipo === 'receita') {
saldoTotal += val;
receitasMes += val;
} else {
saldoTotal -= val;
despesasMes += val;
}
});
}
const ocorrenciasPendentes = ocorrencias ? ocorrencias.filter(o => o.status === 'pendente').length : 0;
document.getElementById('dash-saldo').innerText = `R$ ${saldoTotal.toFixed(2)}`;
document.getElementById('dash-receitas').innerText = `R$ ${receitasMes.toFixed(2)}`;
document.getElementById('dash-despesas').innerText = `R$ ${despesasMes.toFixed(2)}`;
document.getElementById('dash-moradores-count').innerText = moradores ? moradores.length : 0;
document.getElementById('dash-ocorrencias-count').innerText = ocorrenciasPendentes;
renderChart(financeiro || []);
}
function renderChart(transactions) {
const ctx = document.getElementById('financeChart').getContext('2d');
if (financeChartInstance) financeChartInstance.destroy();
const labels = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun'];
const dataReceitas = [0, 0, 0, 0, 0, 0];
const dataDespesas = [0, 0, 0, 0, 0, 0];
if (transactions.length > 0) {
dataReceitas[5] = transactions.filter(t => t.tipo === 'receita').reduce((a, b) => a + parseFloat(b.valor), 0);
dataDespesas[5] = transactions.filter(t => t.tipo === 'despesa').reduce((a, b) => a + parseFloat(b.valor), 0);
} else {
dataReceitas.splice(0, 6, 1200, 1500, 1100, 1800, 2000, 2200);
dataDespesas.splice(0, 6, 800, 900, 700, 1000, 950, 1100);
}
financeChartInstance = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [
{ label: 'Receitas', data: dataReceitas, backgroundColor: '#2ecc71', borderRadius: 4 },
{ label: 'Despesas', data: dataDespesas, backgroundColor: '#e74c3c', borderRadius: 4 }
]
},
options: {
responsive: true,
plugins: {
legend: { position: 'bottom' }
},
scales: {
y: { beginAtZero: true, grid: { color: 'rgba(0,0,0,0.05)' } },
x: { grid: { display: false } }
}
}
});
}
async function renderMoradores() {
const tbody = document.getElementById('moradores-list');
tbody.innerHTML = '<tr><td colspan="5" class="text-center">Carregando...</td></tr>';
const { data, error } = await dbSelect('moradores');
if (error) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-danger">Erro ao carregar dados.</td></tr>';
showToast("Erro ao carregar moradores", "danger");
return;
}
tbody.innerHTML = '';
if (data.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">Nenhum morador cadastrado.</td></tr>';
return;
}
data.forEach(m => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td><div class="fw-bold">${m.nome}</div><small class="text-muted">${m.email || ''}</small></td>
<td>${m.bloco}</td>
<td>${m.apartamento}</td>
<td>${m.telefone || '-'}</td>
<td>
<button class="btn btn-sm btn-outline-danger" onclick="deleteItem('moradores', '${m.id}')">
<i class="fas fa-trash"></i>
</button>
</td>
`;
tbody.appendChild(tr);
});
}
function prepareCreateMorador() {
document.getElementById('form-morador').reset();
}
async function saveMorador(e) {
e.preventDefault();
const nome = document.getElementById('morador-nome').value;
const bloco = document.getElementById('morador-bloco').value;
const apartamento = document.getElementById('morador-apto').value;
const telefone = document.getElementById('morador-telefone').value;
const email = document.getElementById('morador-email').value;
const { error } = await dbInsert('moradores', { nome, bloco, apartamento, telefone, email });
if (error) showToast("Erro ao salvar: " + error.message, "danger");
else {
bootstrap.Modal.getInstance(document.getElementById('modalMorador')).hide();
renderMoradores();
showToast("Morador salvo com sucesso!", "success");
}
}
async function renderFinanceiro() {
const tbody = document.getElementById('financeiro-list');
tbody.innerHTML = '<tr><td colspan="5" class="text-center">Carregando...</td></tr>';
const { data, error } = await dbSelect('financeiro', { col: 'created_at', asc: false });
if (error) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-danger">Erro ao carregar.</td></tr>';
return;
}
tbody.innerHTML = '';
if (data.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">Nenhuma transação encontrada.</td></tr>';
return;
}
data.forEach(t => {
const tr = document.createElement('tr');
const badgeClass = t.tipo === 'receita' ? 'bg-success' : 'bg-danger';
const icon = t.tipo === 'receita' ? 'fa-arrow-up' : 'fa-arrow-down';
tr.innerHTML = `
<td>${new Date(t.data).toLocaleDateString()}</td>
<td>${t.descricao}</td>
<td><span class="badge ${badgeClass}"><i class="fas ${icon} me-1"></i>${t.tipo.toUpperCase()}</span></td>
<td class="${t.tipo === 'receita' ? 'text-success' : 'text-danger'} fw-bold">R$ ${parseFloat(t.valor).toFixed(2)}</td>
<td>
<button class="btn btn-sm btn-outline-danger" onclick="deleteItem('financeiro', '${t.id}')"><i class="fas fa-trash"></i></button>
</td>
`;
tbody.appendChild(tr);
});
}
function prepareCreateTransacao() {
document.getElementById('form-transacao').reset();
}
async function saveTransacao(e) {
e.preventDefault();
const descricao = document.getElementById('transacao-descricao').value;
const tipo = document.getElementById('transacao-tipo').value;
const valor = document.getElementById('transacao-valor').value;
const data = document.getElementById('transacao-data').value;
const { error } = await dbInsert('financeiro', { descricao, tipo, valor, data });
if (error) showToast("Erro: " + error.message, "danger");
else {
bootstrap.Modal.getInstance(document.getElementById('modalTransacao')).hide();
renderFinanceiro();
showToast("Transação registrada!", "success");
}
}
async function renderOcorrencias() {
const container = document.getElementById('ocorrencias-list');
const { data, error } = await dbSelect('ocorrencias', { col: 'created_at', asc: false });
if (error) {
container.innerHTML = '<p class="text-center text-danger col-12">Erro ao carregar</p>';
return;
}
container.innerHTML = '';
if (data.length === 0) {
container.innerHTML = '<div class="col-12 text-center text-muted py-5"><i class="fas fa-check-circle fa-3x mb-3 text-success"></i><p>Nenhuma ocorrência pendente.</p></div>';
return;
}
data.forEach(o => {
const card = document.createElement('div');
card.className = 'col-md-4 mb-4 fade-in';
card.innerHTML = `
<div class="card h-100 shadow-sm custom-card">
${o.imagem_url ? `<img src="${o.imagem_url}" class="card-img-top ocorrencia-img" alt="Ocorrência">` : ''}
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<h5 class="card-title mb-0">${o.titulo}</h5>
<span class="badge bg-warning text-dark">Pendente</span>
</div>
<p class="card-text text-secondary">${o.descricao}</p>
<div class="d-flex justify-content-between align-items-center mt-3">
<small class="text-muted"><i class="far fa-clock me-1"></i>${new Date(o.created_at).toLocaleDateString()}</small>
<button class="btn btn-sm btn-outline-success" onclick="resolveOcorrencia('${o.id}')">Resolver</button>
</div>
</div>
</div>
`;
container.appendChild(card);
});
}
function prepareCreateOcorrencia() {
document.getElementById('form-ocorrencia').reset();
}
async function saveOcorrencia(e) {
e.preventDefault();
const titulo = document.getElementById('ocorrencia-titulo').value;
const descricao = document.getElementById('ocorrencia-descricao').value;
const fileInput = document.getElementById('ocorrencia-file');
let imagem_url = null;
if (IS_MOCK && fileInput.files.length > 0) {
showToast("Upload simulado (Mock Mode)", "info");
imagem_url = "https://placehold.co/600x400?text=Imagem+Ocorrencia";
} else if (fileInput.files.length > 0) {
const file = fileInput.files[0];
const fileName = `${Date.now()}_${file.name}`;
const { data, error } = await supabase.storage.from('condopro-bucket').upload(fileName, file);
if (error) {
showToast('Erro upload imagem: ' + error.message, "danger");
return;
}
const { data: publicData } = supabase.storage.from('condopro-bucket').getPublicUrl(fileName);
imagem_url = publicData.publicUrl;
}
const { error } = await dbInsert('ocorrencias', { titulo, descricao, imagem_url, status: 'pendente' });
if (error) showToast("Erro: " + error.message, "danger");
else {
bootstrap.Modal.getInstance(document.getElementById('modalOcorrencia')).hide();
renderOcorrencias();
showToast("Ocorrência reportada!", "success");
}
}
async function resolveOcorrencia(id) {
await dbDelete('ocorrencias', id);
renderOcorrencias();
showToast("Ocorrência marcada como resolvida!", "success");
}
async function renderAvisos() {
const list = document.getElementById('avisos-list');
const { data, error } = await dbSelect('avisos', { col: 'created_at', asc: false });
if (error) {
list.innerHTML = 'Erro ao carregar';
return;
}
list.innerHTML = '';
if (data.length === 0) {
list.innerHTML = '<p class="text-muted text-center">Nenhum aviso no mural.</p>';
return;
}
data.forEach(a => {
const item = document.createElement('a');
item.className = 'list-group-item list-group-item-action flex-column align-items-start border-start border-4 border-info shadow-sm mb-2';
item.innerHTML = `
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1 text-primary"><i class="fas fa-thumbtack me-2"></i>${a.titulo}</h5>
<small class="text-muted">${new Date(a.created_at).toLocaleDateString()}</small>
</div>
<p class="mb-1 mt-2">${a.mensagem}</p>
`;
list.appendChild(item);
});
}
function prepareCreateAviso() {
document.getElementById('form-aviso').reset();
}
async function saveAviso(e) {
e.preventDefault();
const titulo = document.getElementById('aviso-titulo').value;
const mensagem = document.getElementById('aviso-mensagem').value;
const { error } = await dbInsert('avisos', { titulo, mensagem });
if (error) showToast("Erro: " + error.message, "danger");
else {
bootstrap.Modal.getInstance(document.getElementById('modalAviso')).hide();
renderAvisos();
showToast("Aviso publicado!", "info");
}
}
async function deleteItem(table, id) {
if (confirm('Tem certeza que deseja excluir?')) {
const { error } = await dbDelete(table, id);
if (error) showToast("Erro ao excluir", "danger");
else {
showToast("Item excluído.", "success");
if (table === 'moradores') renderMoradores();
if (table === 'financeiro') renderFinanceiro();
}
}
}
function exportPDF(tableId, filename) {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
doc.text(filename.replace('_', ' '), 14, 15);
doc.autoTable({ html: '#' + tableId, startY: 20 });
doc.save(filename + '.pdf');
showToast("PDF gerado com sucesso!", "success");
}