home
/
u529748449
/
domains
/
borabilhete.com
/
public_html
/
admin
➕ New
📤 Upload
✎ Editing:
dashboard.php
← Back
<?php // Este arquivo **somente renderiza** o dashboard e depende das variáveis já calculadas no index.php: // $paidRevenue, $repasseTotal, $ordersPaid, $ordersPending, $ordersCanceled, // $ticketsSold, $series, $pm, $setoresRows, $pedidos, $from, $to // e das funções brl() e dt() já definidas. ?> <!-- KPIs --> <div class="kpis"> <div class="card kpi ok"> <div class="label">Receita (pagos)</div> <div class="value"><?= brl($paidRevenue) ?></div> <div class="muted">Repasse estimado: <strong><?= brl($repasseTotal) ?></strong></div> <div class="muted">Período: <?= date('d/m/Y', strtotime($from)) ?> – <?= date('d/m/Y', strtotime($to)) ?></div> </div> <div class="card kpi"> <div class="label">Ingressos vendidos</div> <div class="value"><?= number_format($ticketsSold, 0, ',', '.') ?></div> <div class="muted"><?= $ordersPaid ?> pedidos pagos</div> </div> <div class="card kpi warn"> <div class="label">Pendentes</div> <div class="value"><?= number_format($ordersPending, 0, ',', '.') ?></div> <div class="muted">Aguardando pagamento</div> </div> <div class="card kpi bad"> <div class="label">Cancelados</div> <div class="value"><?= number_format($ordersCanceled, 0, ',', '.') ?></div> <div class="muted">Pedidos cancelados</div> </div> </div> <!-- Gráficos --> <div class="grid-2"> <div class="card"> <h3><i class="fa-solid fa-chart-line"></i> Vendas por dia</h3> <canvas id="chartDia"></canvas> </div> <div class="card"> <h3><i class="fa-solid fa-wallet"></i> Por forma de pagamento</h3> <canvas id="chartPM"></canvas> </div> </div> <!-- Vendas por setor --> <div class="card" style="margin-bottom:12px;"> <h3><i class="fa-solid fa-layer-group"></i> Vendas por setor</h3> <table> <thead> <tr> <th>Setor</th> <th>Ingressos</th> <th>Total</th> </tr> </thead> <tbody> <?php if(!$setoresRows): ?> <tr><td colspan="3" class="muted">Sem vendas no período.</td></tr> <?php else: foreach($setoresRows as $s): ?> <tr> <td><?= htmlspecialchars($s['nome_setor']) ?></td> <td><?= (int)$s['ingressos'] ?></td> <td><?= brl($s['total']) ?></td> </tr> <?php endforeach; endif; ?> </tbody> </table> </div> <!-- Pedidos com rolagem --> <div class="card"> <h3><i class="fa-solid fa-receipt"></i> Pedidos</h3> <div class="table-scroll" data-visible-rows="10"> <table class="table-pedidos"> <thead> <tr> <th data-field="id"><div class="th-with-filter">ID <button class="col-filter" type="button" aria-label="Filtrar ID"><i class="fa-solid fa-filter"></i></button></div></th> <th data-field="evento"><div class="th-with-filter">Evento <button class="col-filter" type="button" aria-label="Filtrar Evento"><i class="fa-solid fa-filter"></i></button></div></th> <th data-field="cliente"><div class="th-with-filter">Cliente <button class="col-filter" type="button" aria-label="Filtrar Cliente"><i class="fa-solid fa-filter"></i></button></div></th> <th data-field="status"><div class="th-with-filter">Status <button class="col-filter" type="button" aria-label="Filtrar Status"><i class="fa-solid fa-filter"></i></button></div></th> <th data-field="forma"><div class="th-with-filter">Forma <button class="col-filter" type="button" aria-label="Filtrar Forma"><i class="fa-solid fa-filter"></i></button></div></th> <th data-field="ingressos"><div class="th-with-filter">Ingressos <button class="col-filter" type="button" aria-label="Filtrar Ingressos"><i class="fa-solid fa-filter"></i></button></div></th> <th data-field="total"><div class="th-with-filter">Total <button class="col-filter" type="button" aria-label="Filtrar Total"><i class="fa-solid fa-filter"></i></button></div></th> <th data-field="criado"><div class="th-with-filter">Criado em <button class="col-filter" type="button" aria-label="Filtrar Data"><i class="fa-solid fa-filter"></i></button></div></th> </tr> </thead> <tbody> <?php if(!$pedidos): ?> <tr><td colspan="8" class="muted">Nenhum pedido neste período.</td></tr> <?php else: foreach($pedidos as $u): ?> <tr> <td data-field="id" data-num="<?= (int)$u['id'] ?>">#<?= (int)$u['id'] ?></td> <td data-field="evento" data-val="<?= htmlspecialchars($u['evento']) ?>"><?= htmlspecialchars($u['evento']) ?></td> <td data-field="cliente" data-val="<?= htmlspecialchars($u['nome_cliente']) ?>"><?= htmlspecialchars($u['nome_cliente']) ?></td> <td data-field="status" data-val="<?= ucfirst($u['status_pagamento']) ?>"> <?php $st = $u['status_pagamento']; $cls = $st==='pago' ? 'st-pago' : ($st==='cancelado' ? 'st-cancelado' : 'st-pendente'); ?> <span class="status <?= $cls ?>"><?= ucfirst($st) ?></span> </td> <td data-field="forma" data-val="<?= htmlspecialchars($u['forma']) ?>"><?= htmlspecialchars($u['forma']) ?></td> <td data-field="ingressos" data-num="<?= (int)$u['ingressos'] ?>"><?= (int)$u['ingressos'] ?></td> <td data-field="total" data-num="<?= number_format((float)$u['total'], 2, '.', '') ?>"><?= brl($u['total']) ?></td> <td class="muted" data-field="criado" data-iso="<?= (new DateTime($u['criado_em']))->format('Y-m-d\TH:i') ?>"><?= dt($u['criado_em']) ?></td> </tr> <?php endforeach; endif; ?> </tbody> </table> </div> </div> <!-- Chart.js CDN --> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script> <script> // Dados PHP -> JS const serieLabels = <?= json_encode(array_map(fn($r)=>date('d/m', strtotime($r['dia'])), $series), JSON_UNESCAPED_UNICODE) ?>; const serieData = <?= json_encode(array_map(fn($r)=>(float)$r['total'], $series), JSON_UNESCAPED_UNICODE) ?>; const pmLabels = <?= json_encode(array_map(fn($r)=>$r['forma'], $pm), JSON_UNESCAPED_UNICODE) ?>; const pmValues = <?= json_encode(array_map(fn($r)=>(float)$r['total'], $pm), JSON_UNESCAPED_UNICODE) ?>; // Linha - Vendas por dia new Chart(document.getElementById('chartDia'), { type: 'line', data: { labels: serieLabels, datasets: [{ label: 'R$ por dia (pagos)', data: serieData, tension: 0.35, borderWidth: 2, fill: true }] }, options: { plugins: { legend: { display: false } }, scales: { y: { ticks: { callback: v => 'R$ ' + Number(v).toLocaleString('pt-BR',{minimumFractionDigits:0}) } } } } }); // Doughnut - Formas de pagamento new Chart(document.getElementById('chartPM'), { type: 'doughnut', data: { labels: pmLabels, datasets: [{ data: pmValues, borderWidth: 0 }] }, options: { plugins: { legend: { position: 'bottom' } }, cutout: '60%' } }); </script> <!-- Popover de filtros da tabela "Pedidos" --> <div id="filter-pop" class="filter-pop" role="dialog" aria-hidden="true"></div> <script> (() => { const table = document.querySelector('.table-pedidos'); if(!table) return; // Ajusta a altura do wrapper para exibir ~10 linhas e rolar o restante const wrap = document.querySelector('.table-scroll'); const visibleRows = Number(wrap?.dataset.visibleRows || 10); requestAnimationFrame(() => { const headH = table.tHead?.getBoundingClientRect().height || 40; const row = table.tBodies[0]?.querySelector('tr'); const rowH = row ? row.getBoundingClientRect().height : 48; wrap.style.maxHeight = (headH + rowH * visibleRows + 8) + 'px'; }); const pop = document.getElementById('filter-pop'); const tbody = table.querySelector('tbody'); // Estado dos filtros const state = { id: null, evento: null, cliente: null, status: null, forma: null, ingressos: null, total: null, criado: null }; const getCell = (tr, field) => tr.querySelector(`td[data-field="${field}"]`); const num = (cell) => Number(cell?.dataset.num ?? NaN); const txt = (cell) => (cell?.dataset.val ?? cell?.textContent ?? '').trim(); const date = (cell) => cell?.dataset.iso ? new Date(cell.dataset.iso) : null; function uniqueValues(field){ const vals = new Set(); tbody.querySelectorAll('tr').forEach(tr => { const cell = getCell(tr, field); if(!cell) return; if(['id','ingressos','total','criado'].includes(field)) return; const v = txt(cell); if(v) vals.add(v); }); return Array.from(vals).sort((a,b)=>a.localeCompare(b, 'pt-BR', {numeric:true,sensitivity:'base'})); } function rangeMinMax(field){ let min = +Infinity, max = -Infinity; tbody.querySelectorAll('tr').forEach(tr => { const cell = getCell(tr, field); const v = num(cell); if(!Number.isFinite(v)) return; min = Math.min(min, v); max = Math.max(max, v); }); if(min===+Infinity) min = 0, max = 0; return {min, max}; } function dateBounds(){ let from = null, to = null; tbody.querySelectorAll('tr').forEach(tr => { const d = date(getCell(tr,'criado')); if(!d) return; if(!from || d<from) from = d; if(!to || d>to) to = d; }); return {from, to}; } function showNoResults(show){ let row = tbody.querySelector('tr.no-results'); if(show && !row){ row = document.createElement('tr'); row.className = 'no-results'; const td = document.createElement('td'); td.colSpan = 8; td.className = 'muted'; td.textContent = 'Nenhum resultado com os filtros.'; row.appendChild(td); tbody.appendChild(row); } else if(!show && row){ row.remove(); } } function applyFilters(){ let visible = 0; tbody.querySelectorAll('tr').forEach(tr => { if(tr.classList.contains('no-results')) return; let ok = true; // CATEGÓRICOS for(const f of ['evento','cliente','status','forma']){ if(state[f] && state[f] instanceof Set && state[f].size){ const v = txt(getCell(tr,f)); if(!state[f].has(v)) { ok=false; break; } } } // NUMÉRICOS if(ok && state.id){ const v = num(getCell(tr,'id')); if(Number.isFinite(v)){ if(state.id.min!==null && v < state.id.min) ok=false; if(state.id.max!==null && v > state.id.max) ok=false; } } if(ok && state.ingressos){ const v = num(getCell(tr,'ingressos')); if(state.ingressos.min!==null && v < state.ingressos.min) ok=false; if(state.ingressos.max!==null && v > state.ingressos.max) ok=false; } if(ok && state.total){ const v = num(getCell(tr,'total')); if(state.total.min!==null && v < state.total.min) ok=false; if(state.total.max!==null && v > state.total.max) ok=false; } // DATA if(ok && state.criado){ const d = date(getCell(tr,'criado')); if(d){ if(state.criado.from && d < state.criado.from) ok=false; if(state.criado.to && d > state.criado.to) ok=false; } } tr.style.display = ok ? '' : 'none'; if(ok) visible++; }); showNoResults(visible===0); } function openFilterFor(th){ const field = th.dataset.field; if(!field) return; const rect = th.getBoundingClientRect(); pop.style.left = `${rect.left + window.scrollX}px`; pop.style.top = `${rect.bottom + window.scrollY + 6}px`; pop.innerHTML = ''; const title = document.createElement('h4'); title.textContent = 'Filtrar ' + th.textContent.replace('','').trim(); pop.appendChild(title); if(['evento','cliente','status','forma'].includes(field)){ const values = uniqueValues(field); const chosen = state[field] instanceof Set ? state[field] : new Set(); const list = document.createElement('div'); list.className = 'list'; values.forEach(v => { const row = document.createElement('label'); row.className='row'; const cb = document.createElement('input'); cb.type='checkbox'; cb.value=v; cb.checked = chosen.size ? chosen.has(v) : true; const span = document.createElement('span'); span.textContent = v; row.appendChild(cb); row.appendChild(span); list.appendChild(row); }); pop.appendChild(list); const actions = document.createElement('div'); actions.className='actions'; const clear = document.createElement('button'); clear.className='btn clear'; clear.type='button'; clear.textContent='Limpar'; const apply = document.createElement('button'); apply.className='btn apply'; apply.type='button'; apply.textContent='Aplicar'; clear.onclick = () => { state[field] = null; closePop(); applyFilters(); }; apply.onclick = () => { const selected = Array.from(list.querySelectorAll('input[type="checkbox"]')).filter(i=>i.checked).map(i=>i.value); state[field] = selected.length ? new Set(selected) : null; closePop(); applyFilters(); }; actions.appendChild(clear); actions.appendChild(apply); pop.appendChild(actions); } else if(['id','ingressos','total'].includes(field)){ const {min, max} = rangeMinMax(field); const current = state[field] || {min:null, max:null}; const box = document.createElement('div'); box.className='fields'; const iMin = document.createElement('input'); iMin.type='number'; iMin.step = field==='total' ? '0.01' : '1'; iMin.placeholder = `mín (${min})`; iMin.value = current.min ?? ''; const iMax = document.createElement('input'); iMax.type='number'; iMax.step = field==='total' ? '0.01' : '1'; iMax.placeholder = `máx (${max})`; iMax.value = current.max ?? ''; box.appendChild(iMin); box.appendChild(iMax); pop.appendChild(box); const actions = document.createElement('div'); actions.className='actions'; const clear = document.createElement('button'); clear.className='btn clear'; clear.type='button'; clear.textContent='Limpar'; const apply = document.createElement('button'); apply.className='btn apply'; apply.type='button'; apply.textContent='Aplicar'; clear.onclick = () => { state[field] = null; closePop(); applyFilters(); }; apply.onclick = () => { state[field] = { min: iMin.value!=='' ? Number(iMin.value) : null, max: iMax.value!=='' ? Number(iMax.value) : null }; closePop(); applyFilters(); }; actions.appendChild(clear); actions.appendChild(apply); pop.appendChild(actions); } else if(field==='criado'){ const bounds = dateBounds(); const {from, to} = bounds; const current = state.criado || {from:null, to:null}; const box = document.createElement('div'); box.className='fields'; const f = document.createElement('input'); f.type='datetime-local'; const t = document.createElement('input'); t.type='datetime-local'; if(from){ f.placeholder = 'de'; f.min = iso(from); } if(to){ t.placeholder = 'até'; t.max = iso(to); } if(current.from) f.value = iso(current.from); if(current.to) t.value = iso(current.to); box.appendChild(f); box.appendChild(t); pop.appendChild(box); const actions = document.createElement('div'); actions.className='actions'; const clear = document.createElement('button'); clear.className='btn clear'; clear.type='button'; clear.textContent='Limpar'; const apply = document.createElement('button'); apply.className='btn apply'; apply.type='button'; apply.textContent='Aplicar'; clear.onclick = () => { state.criado = null; closePop(); applyFilters(); }; apply.onclick = () => { state.criado = { from: f.value ? new Date(f.value) : null, to: t.value ? new Date(t.value) : null }; closePop(); applyFilters(); }; actions.appendChild(clear); actions.appendChild(apply); pop.appendChild(actions); function iso(d){ const pad = n => String(n).padStart(2,'0'); return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`; } } pop.dataset.for = field; pop.style.display = 'block'; setTimeout(()=>document.addEventListener('click', outsideHandler)); } function closePop(){ pop.style.display = 'none'; document.removeEventListener('click', outsideHandler); } function outsideHandler(e){ const isButton = e.target.closest('.col-filter'); const isInside = e.target.closest('#filter-pop'); if(isInside) return; if(isButton){ const th = isButton.closest('th'); if(pop.style.display==='block' && pop.dataset.for===th?.dataset.field){ closePop(); return; } openFilterFor(th); return; } closePop(); } table.querySelectorAll('th .col-filter').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const th = btn.closest('th'); openFilterFor(th); }); }); })(); </script>
💾 Save Changes
Cancel
📤 Upload File
×
Select File
Upload
Cancel
➕ Create New
×
Type
📄 File
📁 Folder
Name
Create
Cancel
✎ Rename Item
×
Current Name
New Name
Rename
Cancel
🔐 Change Permissions
×
Target File
Permission (e.g., 0755, 0644)
0755
0644
0777
Apply
Cancel