181 lines
7.6 KiB
JavaScript
181 lines
7.6 KiB
JavaScript
/* Document history table — used on history.html */
|
|
|
|
async function loadHistory() {
|
|
const wrap = document.getElementById('historyTableWrap');
|
|
if (!wrap) return;
|
|
|
|
try {
|
|
const data = await apiCall('list');
|
|
const jobs = data?.data?.jobs || data?.jobs || [];
|
|
renderHistory(jobs);
|
|
} catch (e) {
|
|
console.error('[history] failed to load:', e);
|
|
}
|
|
}
|
|
|
|
function renderHistory(jobs) {
|
|
const wrap = document.getElementById('historyTableWrap');
|
|
const empty = document.getElementById('historyEmpty');
|
|
|
|
if (!jobs.length) {
|
|
if (empty) empty.style.display = '';
|
|
wrap.querySelectorAll('.history-section').forEach(el => el.remove());
|
|
const old = wrap.querySelector('table');
|
|
if (old) old.remove();
|
|
return;
|
|
}
|
|
if (empty) empty.style.display = 'none';
|
|
|
|
// Clear previous content
|
|
wrap.querySelectorAll('.history-section').forEach(el => el.remove());
|
|
const old = wrap.querySelector('table');
|
|
if (old) old.remove();
|
|
|
|
// Group by days remaining (30-day retention)
|
|
const RETENTION_DAYS = 30;
|
|
const now = Date.now();
|
|
|
|
function getDaysRemaining(j) {
|
|
if (!j.uploaded_at) return RETENTION_DAYS;
|
|
const uploaded = new Date(j.uploaded_at).getTime();
|
|
const ageMs = now - uploaded;
|
|
const ageDays = ageMs / (1000 * 60 * 60 * 24);
|
|
return Math.max(0, Math.ceil(RETENTION_DAYS - ageDays));
|
|
}
|
|
|
|
// Sort jobs: soonest-to-expire first
|
|
const sorted = [...jobs].sort((a, b) => getDaysRemaining(a) - getDaysRemaining(b));
|
|
|
|
// Group into buckets
|
|
const buckets = { urgent: [], soon: [], safe: [] };
|
|
sorted.forEach(j => {
|
|
const days = getDaysRemaining(j);
|
|
if (days < 10) buckets.urgent.push(j);
|
|
else if (days < 20) buckets.soon.push(j);
|
|
else buckets.safe.push(j);
|
|
});
|
|
|
|
const bucketConfig = [
|
|
{ key: 'urgent', label: 'Expiring Soon', color: '#ef4444', textColor: 'white' },
|
|
{ key: 'soon', label: 'Expiring', color: '#f59e0b', textColor: 'black' },
|
|
{ key: 'safe', label: 'Retained', color: '#059669', textColor: 'white' },
|
|
];
|
|
|
|
bucketConfig.forEach(({ key, label, color, textColor }) => {
|
|
const group = buckets[key];
|
|
if (!group.length) return;
|
|
|
|
const section = document.createElement('div');
|
|
section.className = 'history-section';
|
|
section.style.marginBottom = '24px';
|
|
|
|
const heading = document.createElement('div');
|
|
heading.style.cssText = `display:flex;align-items:center;gap:8px;margin-bottom:10px;`;
|
|
heading.innerHTML = `
|
|
<span style="background:${color};color:${textColor};padding:3px 10px;border-radius:12px;font-size:12px;font-weight:600;">${label}</span>
|
|
<span style="font-size:13px;color:var(--text-muted);">${group.length} document${group.length !== 1 ? 's' : ''}</span>`;
|
|
section.appendChild(heading);
|
|
|
|
const table = document.createElement('table');
|
|
table.className = 'history-table';
|
|
table.setAttribute('aria-label', `${label} documents`);
|
|
|
|
const rows = group.map(j => buildHistoryRow(j, getDaysRemaining(j))).join('');
|
|
table.innerHTML = `
|
|
<thead><tr>
|
|
<th>Document</th>
|
|
<th>Date</th>
|
|
<th>Status</th>
|
|
<th>Score</th>
|
|
<th>Issues</th>
|
|
<th>Expires in</th>
|
|
<th>Actions</th>
|
|
</tr></thead>
|
|
<tbody>${rows}</tbody>`;
|
|
section.appendChild(table);
|
|
wrap.appendChild(section);
|
|
});
|
|
}
|
|
|
|
function buildHistoryRow(j, daysRemaining) {
|
|
const score = j.score != null ? j.score : '—';
|
|
const grade = j.grade || '—';
|
|
const scoreClass = j.score >= 90 ? 'history-score-a'
|
|
: j.score >= 70 ? 'history-score-b'
|
|
: j.score != null ? 'history-score-f' : '';
|
|
const scoreAdj = j.score_adjusted ? '<small style="color:var(--text-muted);"> adj</small>' : '';
|
|
const status = j.status === 'completed'
|
|
? '<span class="history-badge-done">Done</span>'
|
|
: '<span class="history-badge-pending">Pending</span>';
|
|
const critical = j.critical_count ?? 0;
|
|
const errors = j.error_count ?? 0;
|
|
const date = j.uploaded_at ? j.uploaded_at.replace('T', ' ').substring(0, 16) : '—';
|
|
const name = escapeHtml(j.original_filename || j.job_id);
|
|
|
|
const expiryColor = daysRemaining < 10 ? 'var(--error)' : daysRemaining < 20 ? 'var(--warning)' : 'var(--success)';
|
|
const expiryCell = `<span style="color:${expiryColor};font-weight:600;">${daysRemaining}d</span>`;
|
|
|
|
const openBtn = j.status === 'completed'
|
|
? `<a class="history-action-btn" href="index.html?job_id=${j.job_id}" title="Open report">Open</a>`
|
|
: '';
|
|
const htmlBtn = j.status === 'completed'
|
|
? `<a class="history-action-btn" href="api.php?action=export&job_id=${j.job_id}&format=html" target="_blank">HTML</a>`
|
|
: '';
|
|
const pdfBtn = j.status === 'completed'
|
|
? `<a class="history-action-btn" href="api.php?action=export&job_id=${j.job_id}&format=pdf" target="_blank">PDF</a>`
|
|
: '';
|
|
const jsonBtn = j.status === 'completed'
|
|
? `<a class="history-action-btn" href="api.php?action=export&job_id=${j.job_id}&format=json" target="_blank">JSON</a>`
|
|
: '';
|
|
const deleteBtn = `<button class="history-action-btn history-action-delete" onclick="deleteHistoryJob('${j.job_id}', this)" title="Delete">🗑</button>`;
|
|
|
|
return `<tr>
|
|
<td class="history-filename" title="${escapeHtml(j.original_filename || '')}">${name}</td>
|
|
<td>${date}</td>
|
|
<td>${status}</td>
|
|
<td><span class="history-score ${scoreClass}">${score}${j.score != null ? '<small>/100</small>' : ''}</span>${scoreAdj} <span class="history-grade">${grade}</span></td>
|
|
<td>${critical > 0 ? `<span class="history-crit">${critical} crit</span>` : ''} ${errors > 0 ? `<span class="history-err">${errors} err</span>` : ''}${!critical && !errors && j.status === 'completed' ? '<span style="color:var(--success)">✓ Clean</span>' : ''}</td>
|
|
<td>${expiryCell}</td>
|
|
<td class="history-actions">${openBtn}${htmlBtn}${pdfBtn}${jsonBtn}${deleteBtn}</td>
|
|
</tr>`;
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
return String(str)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
async function deleteHistoryJob(jobId, btn) {
|
|
if (!confirm('Delete this document and its report?')) return;
|
|
btn.disabled = true;
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('job_id', jobId);
|
|
const data = await apiCall('delete', { method: 'POST', body: formData });
|
|
if (data.success) {
|
|
const row = btn.closest('tr');
|
|
const table = row.closest('table');
|
|
const section = table.closest('.history-section');
|
|
row.remove();
|
|
// Remove section if empty
|
|
if (table.querySelector('tbody tr') === null) {
|
|
if (section) section.remove();
|
|
// Show empty state if no sections remain
|
|
const wrap = document.getElementById('historyTableWrap');
|
|
if (wrap && !wrap.querySelector('.history-section')) {
|
|
const empty = document.getElementById('historyEmpty');
|
|
if (empty) empty.style.display = '';
|
|
}
|
|
}
|
|
} else {
|
|
alert('Delete failed: ' + (data.error || 'Unknown error'));
|
|
btn.disabled = false;
|
|
}
|
|
} catch (e) {
|
|
alert('Delete failed.');
|
|
btn.disabled = false;
|
|
}
|
|
}
|