financeiro-dashboard/index.html
Codex CLI a83837a6ac Atualização: Dashboard dinâmico via report.json
Substituído HTML estático por versão dinâmica que carrega dados do report.json via JavaScript.

Benefícios:
- Não precisa mais reescrever todo o HTML para atualizar
- Atualização automática ao atualizar report.json
- Menos risco de erro humano

Fluxo atualizado:
1. Coleta dados do Supabase
2. Gera report.json
3. Dashboard carrega report.json dinamicamente
2026-02-12 13:44:15 +00:00

193 lines
9.7 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</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; }
.table-container { overflow-x: auto; -webkit-overflow-scrolling: touch; }
</style>
</head>
<body>
<div class="container mx-auto px-4 py-6 sm:px-6 lg:px-8">
<div class="text-center mb-8">
<h1 class="text-4xl sm:text-5xl font-bold text-white mb-2">Dashboard Financeiro</h1>
<p class="text-xl text-white/90">Grupo Inova</p>
<p class="text-lg text-white/70" id="date">Carregando...</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div class="bg-white rounded-xl shadow-lg p-6">
<div class="text-gray-500 text-sm mb-1">Total de Hoteis</div>
<div class="text-3xl sm:text-4xl font-bold text-gray-800" id="total-hotels">-</div>
</div>
<div class="bg-white rounded-xl shadow-lg p-6">
<div class="text-gray-500 text-sm mb-1">Custo Total</div>
<div class="text-3xl sm:text-4xl font-bold text-gray-800" id="total-amount">-</div>
</div>
<div class="bg-white rounded-xl shadow-lg p-6">
<div class="text-gray-500 text-sm mb-1">Total Transacoes</div>
<div class="text-3xl sm:text-4xl font-bold text-gray-800" id="total-transactions">-</div>
</div>
<div class="bg-white rounded-xl shadow-lg p-6">
<div class="text-gray-500 text-sm mb-1">Hotel Maior Gasto</div>
<div class="text-3xl sm:text-4xl font-bold text-gray-800" id="highest-hotel">-</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-lg p-6 mb-8">
<input type="text" id="search" class="w-full p-3 border border-gray-300 rounded-lg outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" placeholder="Buscar transacoes...">
</div>
<div id="hotels-container">
<div class="text-center text-gray-500">Carregando dados...</div>
</div>
<div class="text-center mt-12 text-gray-300 text-sm">
<p>&copy; 2026 Grupo Inova</p>
<p>Sistema Financeiro Automatizado</p>
</div>
</div>
<script>
let allHotels = [];
async function loadReport() {
try {
const response = await fetch('report.json');
const data = await response.json();
allHotels = data.hotels;
// Update date
document.getElementById('date').textContent = 'Atualizado: ' + data.date;
// Update stats
document.getElementById('total-hotels').textContent = data.hotels.length;
document.getElementById('total-amount').textContent = 'R$ ' + data.global_total.toLocaleString('pt-BR', {minimumFractionDigits: 2, maximumFractionDigits: 2});
document.getElementById('total-transactions').textContent = data.global_count;
// Find highest hotel
const highest = data.hotels.reduce((prev, current) => (prev.total > current.total) ? prev : current);
document.getElementById('highest-hotel').textContent = highest.name;
// Render hotels
renderHotels(data.hotels);
} catch (error) {
console.error('Erro ao carregar report.json:', error);
document.getElementById('hotels-container').innerHTML = '<div class="text-center text-red-500">Erro ao carregar dados</div>';
}
}
function renderHotels(hotels) {
const container = document.getElementById('hotels-container');
container.innerHTML = '';
hotels.forEach((hotel, index) => {
const hotelId = 'hotel-' + index;
const btnId = 'btn-' + index;
const iconId = 'icon-' + index;
const textId = 'text-' + index;
const hotelHtml = `
<div class="bg-gray-50 border border-gray-200 rounded-xl p-6 mb-8" data-hotel="${hotel.name}">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-4 sm:mb-6">
<div class="flex items-center gap-3 mb-4 sm:mb-0">
<div class="bg-indigo-500 text-white rounded-lg p-2">
<i class="fas fa-building"></i>
</div>
<div>
<div class="text-lg sm:text-xl font-bold text-gray-800">${hotel.name}</div>
</div>
</div>
<div class="text-right sm:text-center sm:mt-0 sm:pt-4">
<div class="text-sm text-gray-600">${hotel.count} transacoes</div>
<div class="text-2xl sm:text-3xl font-bold text-gray-800">R$ ${hotel.total.toLocaleString('pt-BR', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</div>
</div>
</div>
<button id="${btnId}" onclick="toggleHotel('${hotelId}')" class="w-full sm:w-auto bg-white border-2 border-indigo-500 text-indigo-600 hover:bg-indigo-500 hover:text-white font-semibold py-2 px-4 sm:py-3 rounded-lg transition-colors">
<i id="${iconId}" class="fas fa-chevron-down mr-1"></i>
<span id="${textId}">Mostrar Transacoes</span>
</button>
<div id="${hotelId}" class="hidden mt-4 sm:mt-6">
<div class="table-container">
<table class="table-auto w-full text-[10px] sm:text-xs md:text-sm border-collapse">
<thead>
<tr class="border-b">
<th class="px-2 py-2 text-left">Descricao</th>
<th class="px-2 py-2 text-left">Categoria</th>
<th class="px-2 py-2 text-left">Valor</th>
<th class="px-2 py-2 text-left sm:hidden">Fornecedor</th>
<th class="px-2 py-2 text-left sm:hidden">Data</th>
</tr>
</thead>
<tbody>
${hotel.transactions.map(t => `
<tr class="border-b hover:bg-gray-100">
<td class="px-2 py-2">${(t.descricao || '-').substring(0, 50)}</td>
<td class="px-2 py-2"><span class="bg-indigo-100 text-indigo-700 px-2 py-1 rounded text-xs font-semibold">${t.categoria}</span></td>
<td class="px-2 py-2 font-semibold">R$ ${t.valor.toLocaleString('pt-BR', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</td>
<td class="px-2 py-2 sm:hidden">${(t.fornecedor || '-').substring(0, 20)}</td>
<td class="px-2 py-2 sm:hidden">${t.data_vencimento}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
</div>`;
container.innerHTML += hotelHtml;
});
}
function toggleHotel(id) {
const container = document.getElementById(id);
const index = id.split('-')[1];
const btn = document.getElementById('btn-' + index);
const icon = document.getElementById('icon-' + index);
const text = document.getElementById('text-' + index);
if (container.classList.contains('hidden')) {
container.classList.remove('hidden');
icon.className = 'fas fa-chevron-up mr-1';
text.textContent = 'Esconder Transacoes';
} else {
container.classList.add('hidden');
icon.className = 'fas fa-chevron-down mr-1';
text.textContent = 'Mostrar Transacoes';
}
}
// Search functionality
document.getElementById('search').addEventListener('input', function(e) {
const query = e.target.value.toLowerCase();
if (query === '') {
renderHotels(allHotels);
return;
}
const filtered = allHotels.filter(hotel => {
const hotelName = hotel.name.toLowerCase();
const hasMatchingTransaction = hotel.transactions.some(t =>
(t.descricao || '').toLowerCase().includes(query) ||
(t.categoria || '').toLowerCase().includes(query) ||
(t.fornecedor || '').toLowerCase().includes(query)
);
return hotelName.includes(query) || hasMatchingTransaction;
});
renderHotels(filtered);
});
// Load report on page load
loadReport();
</script>
</body>
</html>