Mudanças: - HTML Estático (index.html): Template fixo, não muda - JSON Data (report.json): Dados consolidados - JavaScript: Busca JSON automaticamente - Sistema de Expansão/Colapso Corrigido - Filtro Global Funcional - Design Moderno (Tailwind CSS) Estratégia: - HTML é estático (não precisa de LLM para regerar) - JSON contém os dados diários (gerado pelo job do Supabase) - Zero token diário (apenas custo fixo de banda/dados) - Zero custo LLM (OpenAI/Claude não usado no fluxo diário) - Manutenção mínima (apenas atualizar JSON) Arquivos: - index.html (13KB) - report.json (JSON data) Data: - 7 hotéis - 32 transações - Custo total: R$ 32.282,06
411 lines
13 KiB
HTML
411 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="pt-BR">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Dashboard Financeiro - Grupo Inova</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap');
|
|
|
|
body {
|
|
font-family: 'Plus Jakarta Sans', sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1800px;
|
|
margin: 0 auto;
|
|
padding: 2rem;
|
|
}
|
|
|
|
.card {
|
|
background: white;
|
|
border-radius: 12px;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
padding: 1.5rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.hotel-card {
|
|
background: #f9fafb;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 8px;
|
|
padding: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.hotel-card:hover {
|
|
border-color: #667eea;
|
|
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15);
|
|
}
|
|
|
|
.header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
padding-bottom: 1rem;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
}
|
|
|
|
.hotel-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.name {
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
color: #1f2937;
|
|
}
|
|
|
|
.stats {
|
|
text-align: right;
|
|
}
|
|
|
|
.count {
|
|
font-size: 0.875rem;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.total {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
color: #1f2937;
|
|
}
|
|
|
|
.button {
|
|
background: white;
|
|
border: 1px solid #667eea;
|
|
color: #667eea;
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.button:hover {
|
|
background: #667eea;
|
|
color: white;
|
|
}
|
|
|
|
.transactions {
|
|
display: none;
|
|
margin-top: 1rem;
|
|
padding-top: 1rem;
|
|
border-top: 1px solid #e5e7eb;
|
|
}
|
|
|
|
.transactions.show {
|
|
display: block;
|
|
animation: slideDown 0.3s ease;
|
|
}
|
|
|
|
@keyframes slideDown {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
th {
|
|
background: #f8f9fa;
|
|
padding: 0.5rem;
|
|
text-align: left;
|
|
font-weight: 600;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
td {
|
|
padding: 0.5rem;
|
|
border-bottom: 1px solid #f3f4f6;
|
|
text-align: left;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.row {
|
|
padding: 0.5rem 0;
|
|
border-bottom: 1px solid #f3f4f6;
|
|
}
|
|
|
|
.row:hover {
|
|
background: #f9fafb;
|
|
}
|
|
|
|
.row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 0.25rem 0.75rem;
|
|
background: #667eea;
|
|
color: white;
|
|
border-radius: 4px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.amount {
|
|
font-weight: 600;
|
|
color: #1f2937;
|
|
}
|
|
|
|
.search {
|
|
padding: 0.75rem;
|
|
border: 1px solid #e0e0e0;
|
|
border-radius: 8px;
|
|
width: 100%;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.search:focus {
|
|
outline: none;
|
|
border-color: #667eea;
|
|
box-shadow: 0 0 3px rgba(102, 126, 234, 0.2);
|
|
}
|
|
|
|
.loading {
|
|
text-align: center;
|
|
padding: 2rem;
|
|
color: white;
|
|
}
|
|
|
|
.spinner {
|
|
border: 4px solid rgba(255, 255, 255, 0.3);
|
|
border-radius: 50%;
|
|
border-top: 4px solid white;
|
|
width: 40px;
|
|
height: 40px;
|
|
animation: spin 1s linear infinite;
|
|
margin: 0 auto 1rem;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.error {
|
|
background: #fee2e2;
|
|
color: #dc2626;
|
|
padding: 1rem;
|
|
border-radius: 8px;
|
|
text-align: center;
|
|
margin: 1rem 0;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="text-center mb-8">
|
|
<h1 class="text-4xl font-bold text-white mb-2">Dashboard Financeiro</h1>
|
|
<p class="text-xl text-white/90">Grupo Inova - Squad Financeiro</p>
|
|
<p class="text-lg text-white/70" id="date">Atualizacao: Carregando...</p>
|
|
</div>
|
|
|
|
<div id="loading" class="loading">
|
|
<div class="spinner"></div>
|
|
<p>Carregando dados...</p>
|
|
</div>
|
|
|
|
<div id="dashboard" style="display: none;">
|
|
<div class="card">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<div>
|
|
<p class="text-sm text-gray-500">Total de Hoteis</p>
|
|
<p class="text-3xl font-bold" id="total-hotels">...</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-sm text-gray-500">Custo Total</p>
|
|
<p class="text-3xl font-bold" id="total-amount">...</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-sm text-gray-500">Total Transacoes</p>
|
|
<p class="text-3xl font-bold" id="total-transactions">...</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-sm text-gray-500">Hotel com Maior Gasto</p>
|
|
<p class="text-3xl font-bold" id="highest-hotel">...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<input type="text" id="search" class="search" placeholder="Buscar transacoes...">
|
|
</div>
|
|
|
|
<div id="hotels"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const REPORT_URL = 'report.json';
|
|
|
|
async function init() {
|
|
const loading = document.getElementById('loading');
|
|
const dashboard = document.getElementById('dashboard');
|
|
|
|
try {
|
|
const response = await fetch(REPORT_URL);
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Erro ao carregar dados');
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
loading.style.display = 'none';
|
|
dashboard.style.display = 'block';
|
|
|
|
renderDashboard(data);
|
|
|
|
} catch (error) {
|
|
loading.innerHTML = `
|
|
<div class="error">
|
|
<i class="fas fa-exclamation-triangle text-2xl mb-2"></i>
|
|
<p>Erro ao carregar dados: ${error.message}</p>
|
|
<p>Tente recarregar a página.</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function renderDashboard(data) {
|
|
const { hotels, global_total, global_count, date } = data;
|
|
|
|
document.getElementById('date').textContent = `Atualizacao: ${date}`;
|
|
|
|
const totalHotels = hotels.length;
|
|
const totalAmount = global_total;
|
|
const totalTransactions = global_count;
|
|
|
|
let highestHotel = 'N/A';
|
|
let highestAmount = 0;
|
|
|
|
hotels.forEach(hotel => {
|
|
if (hotel.total > highestAmount) {
|
|
highestAmount = hotel.total;
|
|
highestHotel = hotel.name;
|
|
}
|
|
});
|
|
|
|
document.getElementById('total-hotels').textContent = totalHotels;
|
|
document.getElementById('total-amount').textContent = `R$ ${totalAmount.toFixed(2)}`;
|
|
document.getElementById('total-transactions').textContent = totalTransactions;
|
|
document.getElementById('highest-hotel').textContent = highestHotel;
|
|
|
|
const hotelsContainer = document.getElementById('hotels');
|
|
hotelsContainer.innerHTML = hotels.map((hotel, index) => createHotelCard(hotel, index)).join('');
|
|
}
|
|
|
|
function createHotelCard(hotel, index) {
|
|
const hotelId = `hotel-${index}`;
|
|
const transactionsHtml = hotel.transactions.map(t => createTransactionRow(t)).join('');
|
|
|
|
return `
|
|
<div class="hotel-card">
|
|
<div class="header">
|
|
<div class="hotel-info">
|
|
<i class="fas fa-building text-gray-400"></i>
|
|
<span class="name">${hotel.name}</span>
|
|
</div>
|
|
<div class="stats">
|
|
<span class="count">${hotel.count} transacoes</span>
|
|
<span class="total">R$ ${hotel.total.toFixed(2)}</span>
|
|
</div>
|
|
</div>
|
|
<button class="button" onclick="toggleTransactions('${hotelId}')">
|
|
<i class="fas fa-chevron-down mr-1"></i>
|
|
<span>Mostrar Transacoes</span>
|
|
</button>
|
|
<div id="${hotelId}" class="transactions">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Descricao</th>
|
|
<th>Categoria</th>
|
|
<th>Valor</th>
|
|
<th>Fornecedor</th>
|
|
<th>Data</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${transactionsHtml}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function createTransactionRow(transaction) {
|
|
return `
|
|
<tr class="row">
|
|
<td>${transaction.descricao}</td>
|
|
<td><span class="badge">${transaction.categoria}</span></td>
|
|
<td><span class="amount">R$ ${transaction.valor.toFixed(2)}</span></td>
|
|
<td>${transaction.fornecedor || 'N/A'}</td>
|
|
<td>${transaction.data_vencimento || 'N/A'}</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
function toggleTransactions(hotelId) {
|
|
const container = document.getElementById(hotelId);
|
|
const button = container.querySelector('.button');
|
|
const icon = button.querySelector('i');
|
|
const text = button.querySelector('span');
|
|
|
|
if (container.classList.contains('show')) {
|
|
container.classList.remove('show');
|
|
icon.classList.remove('fa-chevron-up');
|
|
icon.classList.add('fa-chevron-down');
|
|
text.textContent = 'Mostrar Transacoes';
|
|
} else {
|
|
container.classList.add('show');
|
|
icon.classList.remove('fa-chevron-down');
|
|
icon.classList.add('fa-chevron-up');
|
|
text.textContent = 'Esconder Transacoes';
|
|
}
|
|
}
|
|
|
|
document.getElementById('search').addEventListener('input', (e) => {
|
|
const term = e.target.value.toLowerCase();
|
|
const rows = document.querySelectorAll('.row');
|
|
|
|
rows.forEach(row => {
|
|
const text = row.textContent.toLowerCase();
|
|
if (term === '' || text.includes(term)) {
|
|
row.style.display = '';
|
|
} else {
|
|
row.style.display = 'none';
|
|
}
|
|
});
|
|
});
|
|
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|