a9e64b65ce
- 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>
212 lines
10 KiB
PHP
212 lines
10 KiB
PHP
<?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 => ({ '&':'&','<':'<','>':'>','"':'"' }[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'; ?>
|