ui: modernize Downloads file list with compact rows, file-type icons, and hover states

Replace button-based file layout with a clean list view featuring colored
file-type badges (HTML/CSV/JSON/XLSX), formatted sizes, download icons,
hover highlights, and category headers with emoji icons. Collapsible
report cards now show file count and use chevron rotation animation.
This commit is contained in:
nogueiraguh
2026-03-07 18:32:15 -03:00
parent c7c4ca1f1b
commit a05956a726

View File

@@ -1231,6 +1231,13 @@ ${tenancies.map(t=>`<option value="${t}" ${S.rptHistFilter===t?'selected':''}>${
${!filtered.length?'<div class="emp" style="padding:.5rem"><p style="font-size:.72rem;color:var(--t4)">Nenhum relatório.</p></div>':''}</div>`}
/* ── Downloads (file browser only) ── */
function _dlFmtSize(b){if(b>=1048576)return(b/1048576).toFixed(1)+' MB';if(b>=1024)return(b/1024).toFixed(1)+' KB';return b+' B'}
function _dlFileIcon(name){
const ext=name.split('.').pop().toLowerCase();
const colors={html:'#e44d26',htm:'#e44d26',csv:'#217346',json:'#f5a623',xlsx:'#217346',xls:'#217346',txt:'#6b7280',pdf:'#dc2626',png:'#8b5cf6',svg:'#8b5cf6'};
const c=colors[ext]||'var(--ac)';
return`<div style="width:28px;height:28px;border-radius:6px;background:${c}14;display:flex;align-items:center;justify-content:center;flex-shrink:0"><span style="font-size:.58rem;font-weight:700;color:${c};text-transform:uppercase;letter-spacing:.3px">${ext}</span></div>`;
}
function rDl(){
const comp=S.reports.filter(r=>r.status==='completed');
const tenancies=[...new Set(comp.map(r=>r.tenancy_name))].sort();
@@ -1247,23 +1254,31 @@ ${!filtered.length?'<div class="cd"><div class="emp"><div class="eic">📂</div>
<div style="display:flex;flex-direction:column;gap:.5rem">${filtered.map(r=>{
const isExp=S.dlExpandedRid===r.id;
const fHtml=isExp&&S.reportFiles[r.id]?renderFileList(r.id,S.reportFiles[r.id]):'';
const fCount=S.reportFiles[r.id]?S.reportFiles[r.id].length:null;
return`<div class="cd" style="padding:0;overflow:hidden">
<div style="display:flex;justify-content:space-between;align-items:center;padding:.6rem .8rem;cursor:pointer" onclick="toggleFiles('${r.id}')">
<div><strong style="font-size:.8rem">${r.tenancy_name}</strong><span style="font-size:.7rem;color:var(--t4);margin-left:.5rem">Level ${r.level||2}${r.created_at}</span></div>
<span style="font-size:.7rem;color:var(--ac)">${isExp?'▲ Ocultar':'▼ Ver Arquivos'}</span></div>
<div style="display:flex;justify-content:space-between;align-items:center;padding:.55rem .8rem;cursor:pointer;transition:background .15s" onmouseover="this.style.background='var(--bg2)'" onmouseout="this.style.background=''" onclick="toggleFiles('${r.id}')">
<div style="display:flex;align-items:center;gap:.5rem">
<svg viewBox="0 0 16 16" width="14" height="14" fill="var(--ac)" style="flex-shrink:0;transition:transform .2s;transform:rotate(${isExp?'90':'0'}deg)"><path d="M6 3l5 5-5 5z"/></svg>
<div><strong style="font-size:.78rem">${r.tenancy_name}</strong>
<div style="font-size:.66rem;color:var(--t4);margin-top:1px">Level ${r.level||2} · ${r.created_at}${fCount!=null?' · '+fCount+' arquivo(s)':''}</div></div></div>
<span style="font-size:.66rem;color:var(--t4)">${isExp?'Ocultar':'Ver Arquivos'}</span></div>
${isExp?fHtml:''}</div>`}).join('')}</div>`}`}
async function toggleFiles(rid){if(S.dlExpandedRid===rid){S.dlExpandedRid=null;R();return}
S.dlExpandedRid=rid;if(!S.reportFiles[rid]){try{S.reportFiles[rid]=await $api('/reports/'+rid+'/files')}catch(e){S.reportFiles[rid]=[]}}R()}
function renderFileList(rid,files){if(!files.length)return'<div style="padding:.6rem;color:var(--t4);font-size:.72rem">Nenhum arquivo encontrado</div>';
function renderFileList(rid,files){if(!files.length)return'<div style="padding:.8rem;color:var(--t4);font-size:.72rem;text-align:center">Nenhum arquivo encontrado</div>';
const groups={};files.forEach(f=>{if(!groups[f.file_category])groups[f.file_category]=[];groups[f.file_category].push(f)});
const labels={summary:'Summary Reports',cis_finding:'CIS Findings',obp_finding:'OBP Findings',obp_best_practice:'OBP Best Practices',raw_data:'Raw Data',error:'Error Reports',diagram:'Diagrams',consolidated:'Consolidated',other:'Other'};
let h='<div style="padding:.6rem .8rem;background:var(--bg2);border-top:1px solid var(--b2)">';
const icons={summary:'📊',cis_finding:'🛡️',obp_finding:'⚠️',obp_best_practice:'✅',raw_data:'📋',error:'❌',diagram:'📈',consolidated:'📑',other:'📄'};
let h='<div style="border-top:1px solid var(--bd);background:var(--bg2)">';
for(const[cat,cf] of Object.entries(groups)){
h+=`<div style="margin-bottom:.5rem"><div style="font-size:.7rem;font-weight:600;color:var(--t3);margin-bottom:.3rem">${labels[cat]||cat} (${cf.length})</div>
<div style="display:flex;flex-wrap:wrap;gap:.3rem">`;
cf.forEach(f=>{const kb=Math.round(f.file_size/1024);
h+=`<a href="${API}/reports/${rid}/files/${f.id}/download" target="_blank" class="btn bs bsm" style="font-size:.64rem;text-decoration:none">${f.file_name} (${kb}KB)</a>`});
h+='</div></div>'}
h+=`<div style="padding:.4rem .8rem .1rem"><div style="font-size:.64rem;font-weight:600;color:var(--t4);text-transform:uppercase;letter-spacing:.4px;margin-bottom:.25rem">${icons[cat]||'📄'} ${labels[cat]||cat} (${cf.length})</div></div>`;
cf.forEach(f=>{
h+=`<a href="${API}/reports/${rid}/files/${f.id}/download" target="_blank" style="display:flex;align-items:center;gap:.5rem;padding:.35rem .8rem;text-decoration:none;color:inherit;transition:background .12s;cursor:pointer" onmouseover="this.style.background='var(--bg3)'" onmouseout="this.style.background=''">
${_dlFileIcon(f.file_name)}
<div style="flex:1;min-width:0;overflow:hidden"><div style="font-size:.72rem;font-weight:500;color:var(--t1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${f.file_name}</div></div>
<span style="font-size:.62rem;color:var(--t4);flex-shrink:0">${_dlFmtSize(f.file_size)}</span>
<svg viewBox="0 0 16 16" width="12" height="12" style="flex-shrink:0;opacity:.35"><path d="M8 1v9m0 0L5 7m3 3l3-3M2 12v1.5A1.5 1.5 0 003.5 15h9a1.5 1.5 0 001.5-1.5V12" stroke="var(--t3)" stroke-width="1.5" fill="none" stroke-linecap="round"/></svg></a>`});
}
h+='</div>';return h}
function dlRpt(id,f){window.open(API+'/reports/'+id+'/download?fmt='+f,'_blank')}
async function refreshDl(){S.reports=await $api('/reports');S.reportFiles={};S.dlExpandedRid=null;R()}