Files
dobetternorge-tools/dashboard/documents.php
T
daveadmin a9e64b65ce i18n all /dashboard/ corpus pages for en/no/uk/pl
- Add require_once bootstrap.php to all 6 dashboard page files so
  dbnToolsT() is available before layout_dashboard.php is included
- Add dash_upload_category_lbl key to no/uk/pl sections of i18n.php
  (was only in English); Kategori/Категорія/Kategoria
- Fix broken ternary on upload.php Category label — replace with
  dbnToolsT('dash_upload_category_lbl', $uiLang)
- layout_dashboard.php outputs window.DBN_I18N with all js_* keys
  so dashboard JS reads locale-aware strings from PHP translations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:10:57 +02:00

212 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
require_once __DIR__ . '/../includes/bootstrap.php';
$dashboardPage = 'documents';
$dashboardTitle = dbnToolsT('dash_title_docs', dbnToolsCurrentLanguage());
$dashboardLead = dbnToolsT('dash_lead_docs', dbnToolsCurrentLanguage());
require_once __DIR__ . '/../includes/layout_dashboard.php';
?>
<section class="dash-card">
<div class="dash-filters">
<input type="search" id="docFilterQ" placeholder="<?= htmlspecialchars(dbnToolsT('dash_filter_q_ph', $uiLang)) ?>" autocomplete="off">
<select id="docFilterStatus">
<option value=""><?= htmlspecialchars(dbnToolsT('dash_filter_all_status', $uiLang)) ?></option>
<option value="ready" id="optReady"></option>
<option value="pending" id="optPending"></option>
<option value="processing" id="optProcessing"></option>
<option value="error" id="optError"></option>
</select>
<button id="docBulkDelete" class="dash-btn dash-btn--danger" disabled><?= htmlspecialchars(dbnToolsT('dash_delete_selected', $uiLang)) ?></button>
<a href="/dashboard/upload.php" class="dash-btn dash-btn--primary" style="margin-left: auto;"><?= htmlspecialchars(dbnToolsT('dash_upload_btn_short', $uiLang)) ?></a>
</div>
<div id="docListWrap"><p class="dash-loading"></p></div>
<div class="dash-pager" id="docPager" hidden>
<span id="docPagerLabel"></span>
<div class="dash-pager__actions">
<button class="dash-btn" id="docPagerPrev"><?= htmlspecialchars(dbnToolsT('dash_prev', $uiLang)) ?></button>
<button class="dash-btn" id="docPagerNext"><?= htmlspecialchars(dbnToolsT('dash_next', $uiLang)) ?></button>
</div>
</div>
</section>
<script>
(function () {
'use strict';
const I18N = window.DBN_I18N || {};
const api = window.DBN_DASHBOARD.apiBase;
const loc = I18N.locale || 'en-GB';
const PAGE = 25;
const optReady = document.getElementById('optReady');
const optPend = document.getElementById('optPending');
const optProc = document.getElementById('optProcessing');
const optErr = document.getElementById('optError');
if (optReady) optReady.textContent = I18N.status_ready || 'Ready';
if (optPend) optPend.textContent = I18N.status_pending || 'Pending';
if (optProc) optProc.textContent = I18N.status_processing || 'Processing';
if (optErr) optErr.textContent = I18N.status_error || 'Error';
const state = { offset: 0, total: 0, selected: new Set(), q: '', status: '' };
const $wrap = document.getElementById('docListWrap');
const $pager = document.getElementById('docPager');
const $pl = document.getElementById('docPagerLabel');
const $prev = document.getElementById('docPagerPrev');
const $next = document.getElementById('docPagerNext');
const $bulk = document.getElementById('docBulkDelete');
const $fq = document.getElementById('docFilterQ');
const $fs = document.getElementById('docFilterStatus');
function safe(s) { return String(s ?? '').replace(/[&<>"]/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[c])); }
function fmtDate(s) {
if (!s) return '—';
try { return new Date(s.replace(' ', 'T') + 'Z').toLocaleDateString(loc, { day:'numeric', month:'short', year:'numeric' }); }
catch (_) { return s; }
}
function fmtNum(n) { return n == null ? '—' : Number(n).toLocaleString(loc); }
function statusPill(status) {
const cls = { ready:'dash-status--ready', pending:'dash-status--pending', processing:'dash-status--processing', error:'dash-status--error' }[status] || 'dash-status--pending';
const lbl = {
ready: I18N.status_ready || 'Ready', pending: I18N.status_pending || 'Pending',
processing: I18N.status_processing || 'Processing', error: I18N.status_error || 'Error',
}[status] || status;
return '<span class="dash-status ' + cls + '">' + lbl + '</span>';
}
function load() {
const qs = new URLSearchParams({ action:'list', offset:String(state.offset), limit:String(PAGE) });
if (state.q) qs.set('q', state.q);
if (state.status) qs.set('status', state.status);
$wrap.innerHTML = '<p class="dash-loading">' + (I18N.loading_docs || 'Loading documents…') + '</p>';
fetch(api + '/documents.php?' + qs, { credentials:'same-origin' })
.then(r => r.json())
.then(data => {
if (!data.ok) throw new Error(data.error?.message || 'Failed');
state.total = data.total;
render(data.documents || []);
})
.catch(err => {
$wrap.innerHTML = '<div class="dash-error">' + safe(err.message) + '</div>';
});
}
function render(docs) {
if (!docs.length) {
$wrap.innerHTML = '<div class="dash-empty"><span class="dash-empty__icon">📭</span>'
+ (state.q || state.status
? (I18N.empty_filter || 'No results for selected filter.')
: (I18N.empty_docs || 'No documents yet.') + ' <a href="/dashboard/upload.php">' + (I18N.empty_docs_link || 'Upload your first') + '</a>.')
+ '</div>';
$pager.hidden = true;
return;
}
const table = document.createElement('table');
table.className = 'dash-doctable';
table.innerHTML =
'<thead><tr>'
+ '<th style="width:36px;"><input type="checkbox" id="docSelectAll"></th>'
+ '<th>' + (I18N.th_title || 'Title') + '</th>'
+ '<th>' + (I18N.th_category || 'Category') + '</th>'
+ '<th>' + (I18N.th_status || 'Status') + '</th>'
+ '<th>' + (I18N.th_chunks || 'Passages') + '</th>'
+ '<th>' + (I18N.th_added || 'Added') + '</th>'
+ '</tr></thead>';
const tbody = document.createElement('tbody');
docs.forEach(doc => {
const tr = document.createElement('tr');
tr.dataset.id = String(doc.id);
tr.innerHTML =
'<td><input type="checkbox" class="doc-check" value="' + doc.id + '"' + (state.selected.has(doc.id) ? ' checked' : '') + '></td>'
+ '<td><span class="dash-doctable__title">' + safe(doc.title) + '</span>'
+ (doc.source_tool ? '<div class="dash-doctable__meta">via ' + safe(doc.source_tool) + (doc.tags ? ' · ' + safe(doc.tags) : '') + '</div>' : (doc.tags ? '<div class="dash-doctable__meta">' + safe(doc.tags) + '</div>' : ''))
+ '</td>'
+ '<td>' + safe(doc.category || '—') + '</td>'
+ '<td>' + statusPill(doc.status) + '</td>'
+ '<td>' + fmtNum(doc.chunk_count) + '</td>'
+ '<td>' + fmtDate(doc.created_at) + '</td>';
tr.addEventListener('click', (e) => {
if (e.target.matches('input[type="checkbox"]')) return;
location.href = '/dashboard/document.php?id=' + doc.id;
});
tbody.appendChild(tr);
});
table.appendChild(tbody);
$wrap.innerHTML = '';
$wrap.appendChild(table);
const all = document.getElementById('docSelectAll');
all.addEventListener('change', () => {
tbody.querySelectorAll('.doc-check').forEach(c => {
c.checked = all.checked;
const id = parseInt(c.value, 10);
if (all.checked) state.selected.add(id); else state.selected.delete(id);
});
updateBulk();
});
tbody.querySelectorAll('.doc-check').forEach(c => {
c.addEventListener('change', (e) => {
const id = parseInt(e.target.value, 10);
if (e.target.checked) state.selected.add(id); else state.selected.delete(id);
updateBulk();
});
});
const from = state.offset + 1;
const to = Math.min(state.offset + docs.length, state.total);
const tmpl = I18N.pager_showing || 'Showing {from}{to} of {total}';
$pl.textContent = tmpl.replace('{from}', from).replace('{to}', to).replace('{total}', state.total);
$prev.disabled = state.offset === 0;
$next.disabled = state.offset + PAGE >= state.total;
$pager.hidden = false;
}
function updateBulk() {
$bulk.disabled = state.selected.size === 0;
$bulk.textContent = state.selected.size > 0
? (I18N.delete_n_selected || 'Delete selected ({n})').replace('{n}', state.selected.size)
: (I18N.delete_selected || 'Delete selected');
}
$prev.addEventListener('click', () => { state.offset = Math.max(0, state.offset - PAGE); load(); });
$next.addEventListener('click', () => { state.offset += PAGE; load(); });
let filterTimer = null;
$fq.addEventListener('input', () => {
clearTimeout(filterTimer);
filterTimer = setTimeout(() => { state.q = $fq.value.trim(); state.offset = 0; load(); }, 250);
});
$fs.addEventListener('change', () => { state.status = $fs.value; state.offset = 0; load(); });
$bulk.addEventListener('click', () => {
if (!state.selected.size) return;
const msg = (I18N.delete_docs_confirm || 'Delete {n} documents? This cannot be undone.').replace('{n}', state.selected.size);
if (!confirm(msg)) return;
const ids = Array.from(state.selected);
$bulk.disabled = true;
fetch(api + '/documents.php?action=delete', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids }),
})
.then(r => r.json())
.then(data => {
if (!data.ok) throw new Error(data.error?.message || 'Delete failed');
state.selected.clear();
updateBulk();
load();
})
.catch(err => alert(err.message));
});
load();
})();
</script>
<?php require_once __DIR__ . '/../includes/layout_dashboard_footer.php'; ?>