Compare commits
2 Commits
f4577dec9e
...
aa64354c06
| Author | SHA1 | Date | |
|---|---|---|---|
| aa64354c06 | |||
| 404bcf8637 |
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"liveServer.settings.port": 5502
|
||||
}
|
||||
79
README.md
79
README.md
@@ -1 +1,78 @@
|
||||
# GestorCondominio
|
||||
# CondoMaster Pro
|
||||
|
||||

|
||||
|
||||
O **CondoMaster Pro** é uma aplicação web moderna e responsiva (Single Page Application - SPA) desenvolvida para simplificar e digitalizar a gestão de condomínios. Desenhado a pensar tanto na entidade gestora (Administradores) como nos habitantes (Moradores), o sistema integra todas as comunicações, finanças e ocorrências do dia a dia.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Funcionalidades Principais
|
||||
|
||||
### Acesso Baseado em Perfis (Role-Based Access Control)
|
||||
* **🧑💻 Administradores (`admin`)**: Visão 360º. Podem gerir moradores, registar receitas e despesas globais, cobrar dívidas, enviar faturas manuais (com um clique) e alterar papéis de acesso ("promover" ou "despromover").
|
||||
* **🏠 Moradores (`morador`)**: Painel simplificado desenhado para transparência. Permite verificar as próprias quotas em atraso, reportar danos/anomalias (manutenção) e reservar espaços comuns.
|
||||
|
||||
### 💰 Faturação e Gestão Financeira (Exclusivo Admins)
|
||||
- Visão geral completa de Fluxo de Caixa (Despesas vs. Receitas).
|
||||
- Emissão instantânea de recibos avulso.
|
||||
- Notificações de dívidas encaminhadas com apenas um clique na tabela integrada de **Faturação**.
|
||||
|
||||
### 📅 Gestão de Reservas
|
||||
* **Lista e Mapa**: Sistema visual de reservas em três ginásios, salões de festas e parques de jogos.
|
||||
* **Histórico Completo**: Página exclusiva para listagem de todas as reservas agendadas, acessível a todas as entidades.
|
||||
|
||||
### 🛠️ Ocorrências e Manutenção
|
||||
- Secção para os condóminos relatarem problemas no edifício (ex: candeeiros partidos, problemas de elevador) indicando o grau de severidade.
|
||||
- Os administradores avaliam a prioridade, resolvem as ocorrências digitalmente e mantêm os residentes notificados do estado.
|
||||
|
||||
### 🎨 Design Moderno & UI Inteligente
|
||||
* Compatível com **Mobile e Desktop**.
|
||||
* Inclui um switch suave para **Modo Escuro (Dark Mode)**, Modo Claro e deteção por Sistema, integrados perfeitamente no menu de perfil.
|
||||
* Sistema de notificações do tipo Themed/Toasts para validações imediatas (Confirmações, Erros, Avisos).
|
||||
|
||||
---
|
||||
|
||||
## 💻 Stack Tecnológica
|
||||
|
||||
O projeto foi construído usando uma arquitetura modular moderna num formato de ficheiro de entrada principal que integra os ecossistemas:
|
||||
|
||||
* **React**: Implementado diretamente do navegador (sem build step local). Geração de componentes declarativos (UI Dinâmica).
|
||||
* **Tailwind CSS**: Carregado dinamicamente para aplicar estilos sofisticados e reativos, acelerando o desenvolvimento visual da interface.
|
||||
* **Lucide React**: Biblioteca adotada inteiramente para a vasta panóplia de ícones (`lucide-react`).
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Como Iniciar (Quick Start)
|
||||
|
||||
Visto que o projeto já traz toda a lógica baseada na Web injetada, não é precisa uma instalação exaustiva na máquina.
|
||||
|
||||
1. **Baixar o Projeto:**
|
||||
Basta que tenhas o ficheiro principal (geralmente `index.html`) e o ambiente disponível na mesma pasta (neste caso `GestorCondominio`).
|
||||
|
||||
2. **Abrir a Aplicação:**
|
||||
- Para pré-visualizar rapidamente a aplicação, podes apenas fazer duplo-clique no **`index.html`** para abrir o sistema num browser moderno.
|
||||
- Alternativamente, podes hospedar este ficheiro num serviço de Live Server ou num host online (ex: Vercel, Netlify, Github Pages), não existindo configuração complexa.
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Credenciais de Base (Ambiente de Testes)
|
||||
|
||||
Neste momento as credenciais estão pré-programadas para experimentação do comportamento do sistema:
|
||||
|
||||
**Acesso de Administrador:**
|
||||
- **Email:** `administradores@gmail.com`
|
||||
- **Palavra-passe:** `admin123`
|
||||
|
||||
**Acesso de Morador:**
|
||||
- **Email:** `moradores@gmail.com`
|
||||
- **Palavra-passe:** `moradores123`
|
||||
|
||||
*(Nota: Alguns moradores registados na base de dados fictícia no "Estado" da app podem aceder através da palavra-passe padrão de morador ou usando o respetivo contacto telefónico)*.
|
||||
|
||||
---
|
||||
|
||||
## 👨🔧 Desenvolvimento e Melhorias Mapeadas
|
||||
* Ligação completa de base de dados escalável com a inicialização nativa contida do **Firebase**.
|
||||
* Emissão e importação de documentos faturação automatizados PDF.
|
||||
|
||||
***Desenvolvido para criar comunidades perfeitamente ligadas.***
|
||||
|
||||
1651
index.html
Normal file
1651
index.html
Normal file
File diff suppressed because it is too large
Load Diff
20
manifest.json
Normal file
20
manifest.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "CondoSync Gestão",
|
||||
"short_name": "CondoSync",
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"background_color": "#2c3e50",
|
||||
"theme_color": "#2c3e50",
|
||||
"icons": [
|
||||
{
|
||||
"src": "https://cdn-icons-png.flaticon.com/512/609/609803.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "https://cdn-icons-png.flaticon.com/512/609/609803.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
492
script.js
Normal file
492
script.js
Normal file
@@ -0,0 +1,492 @@
|
||||
|
||||
|
||||
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");
|
||||
}
|
||||
81
style.css
Normal file
81
style.css
Normal file
@@ -0,0 +1,81 @@
|
||||
:root {
|
||||
--sidebar-width: 250px;
|
||||
--primary-color: #3498db;
|
||||
--secondary-color: #2c3e50;
|
||||
--background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: var(--sidebar-width);
|
||||
background-color: var(--secondary-color);
|
||||
min-height: 100vh;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
margin-bottom: 5px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.nav-link:hover, .nav-link.active {
|
||||
background-color: var(--primary-color) !important;
|
||||
color: white !important;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.nav-link i {
|
||||
width: 25px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
transition: transform 0.2s ease-in-out, box-shadow 0.2s;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.sidebar.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#app-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.transition-width {
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.ocorrencia-img {
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
28
sw.js
Normal file
28
sw.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const CACHE_NAME = 'condopro-v1';
|
||||
const ASSETS_TO_CACHE = [
|
||||
'./',
|
||||
'./index.html',
|
||||
'./style.css',
|
||||
'./script.js',
|
||||
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css',
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css',
|
||||
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2',
|
||||
'https://cdn.jsdelivr.net/npm/chart.js',
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js',
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.23/jspdf.plugin.autotable.min.js'
|
||||
];
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME)
|
||||
.then((cache) => cache.addAll(ASSETS_TO_CACHE))
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then((response) => response || fetch(event.request))
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user