PDF-accessibility-saas/js/history.js

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">&#x1F5D1;</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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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;
}
}