PDF link target=_blank + nginx timeouts + priority filteri (samo s podacima)
nginx (sport.rinet.one): - proxy_read_timeout 60s → 300s - proxy_send_timeout 300s - proxy_buffering off (PDF stream) - client_max_body_size 50M → 100M Endpoints: - /api/v2/klubovi/financirani: +with_data filter (samo s potporama/godišnjakom/HNS) - /api/v2/sportasi/filtered: +samo_priority +samo_s_hns Frontend: - PDF link target=_blank rel=noopener - window._klub_only_priority = true (default) - window._sportas_only_priority = true (default) DB View: - pgz_sport.v_nogomet_priority (prima_potpore, u_godisnjaku, ima_hns_roster)
This commit is contained in:
+194
-14
@@ -1,9 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
PGŽ Sport — Dokumenti UI v1.0
|
||||
PGŽ Sport — Dokumenti UI v1.1
|
||||
dradulic@outlook.com / damir@rinet.one — 2026-05-05
|
||||
Library svih dokumenata: godišnjaci, publikacije, pravilnici, javni pozivi.
|
||||
Tabovi + filteri + drill-down modal s RAG citatima + XLSX export + globalna pretraga.
|
||||
Library svih dokumenata: godišnjaci, publikacije, pravilnici, javni pozivi,
|
||||
+ unified "Svi dokumenti" view (tip × izdavatelj) preko /api/v2/dokumenti/unified.
|
||||
Tabovi + filteri + drill-down modal s RAG citatima + inline PDF iframe + XLSX export.
|
||||
-->
|
||||
<html lang="hr">
|
||||
<head>
|
||||
@@ -150,9 +151,16 @@
|
||||
.modal-bg.open { display:flex; }
|
||||
.modal {
|
||||
background:var(--bg1); border:1px solid #2a3144; border-radius:12px;
|
||||
width:100%; max-width:880px; padding:0; overflow:hidden;
|
||||
width:100%; max-width:1100px; padding:0; overflow:hidden;
|
||||
box-shadow:0 12px 60px rgba(0,0,0,0.6);
|
||||
}
|
||||
.pdf-frame {
|
||||
width:100%; height:72vh; border:0; background:#0a0c12; border-radius:6px;
|
||||
}
|
||||
.pdf-fallback {
|
||||
padding:30px; text-align:center; color:var(--muted);
|
||||
background:var(--bg2); border-radius:6px;
|
||||
}
|
||||
.modal-h {
|
||||
padding:18px 22px; border-bottom:1px solid var(--line); display:flex;
|
||||
align-items:flex-start; justify-content:space-between; gap:12px;
|
||||
@@ -239,6 +247,7 @@
|
||||
</div>
|
||||
<div class="modal-tabs">
|
||||
<button class="modal-tab active" data-mtab="info" onclick="setModalTab('info')">ℹ️ Info</button>
|
||||
<button class="modal-tab" data-mtab="pdf" onclick="setModalTab('pdf')">📄 PDF</button>
|
||||
<button class="modal-tab" data-mtab="search" onclick="setModalTab('search')">🔎 Pretraži sadržaj</button>
|
||||
<button class="modal-tab" data-mtab="citati" onclick="setModalTab('citati')">📎 Citati</button>
|
||||
</div>
|
||||
@@ -254,7 +263,9 @@
|
||||
// Tab definitions (vrste mapping)
|
||||
// ─────────────────────────────────────────
|
||||
const TABS = [
|
||||
{ id:'godisnjaci', label:'📅 Godišnjaci ZSPGZ',
|
||||
{ id:'svi', label:'🗂️ Svi dokumenti',
|
||||
unified:true, view:'cards' },
|
||||
{ id:'godisnjaci', label:'📅 Godišnjaci ZSPGZ',
|
||||
vrste:['godisnjak'], view:'cards-year' },
|
||||
{ id:'publikacije', label:'📰 Publikacije',
|
||||
vrste:['program','plan','strategija','izvjestaj','raspodjela','erasmus'], view:'cards' },
|
||||
@@ -269,6 +280,28 @@ const TABS = [
|
||||
view:'cards' },
|
||||
];
|
||||
|
||||
// Unified library — derived classifications from the API
|
||||
const TIP_OPTIONS = [
|
||||
{ v:'', l:'Svi tipovi' },
|
||||
{ v:'godisnjak', l:'Godišnjak' },
|
||||
{ v:'publikacija', l:'Publikacija' },
|
||||
{ v:'pravilnik', l:'Pravilnik / Statut / Zakon' },
|
||||
{ v:'javni-poziv', l:'Javni poziv' },
|
||||
{ v:'natjecaj', l:'Natjecaj' },
|
||||
{ v:'ostalo', l:'Ostalo' },
|
||||
];
|
||||
const IZDAVATELJ_OPTIONS = [
|
||||
{ v:'', l:'Svi izdavatelji' },
|
||||
{ v:'ZSPGZ', l:'Zajednica sportova PGŽ' },
|
||||
{ v:'PGŽ', l:'PGŽ (Županija)' },
|
||||
{ v:'RSS', l:'Riječki sportski savez' },
|
||||
{ v:'HOO', l:'Hrvatski olimpijski odbor' },
|
||||
{ v:'JLS', l:'Grad / Općina (JLS)' },
|
||||
{ v:'savez', l:'Nacionalni sportski savez' },
|
||||
{ v:'klub', l:'Klub' },
|
||||
{ v:'ostalo',l:'Ostalo' },
|
||||
];
|
||||
|
||||
const VRSTE_HIDDEN = ['corpus','corpus_v2','corpus_v3','novost_savez','audit','godisnjak_facts'];
|
||||
const API = '/sport/api/v2'; // hosted under /sport/ via reverse proxy
|
||||
// Fallback: when running directly on :8095, /sport prefix is stripped — try both
|
||||
@@ -277,13 +310,16 @@ const API_DIRECT = '/api/v2';
|
||||
// ─────────────────────────────────────────
|
||||
// State
|
||||
// ─────────────────────────────────────────
|
||||
let activeTab = 'godisnjaci';
|
||||
let activeTab = 'svi';
|
||||
let allDocs = []; // all loaded docs (cached after first fetch)
|
||||
let unifiedDocs = []; // separately cached unified-view docs (with tip/izdavatelj)
|
||||
let unifiedFacets = null; // counts {by_tip, by_izdavatelj}
|
||||
let displayDocs = []; // currently filtered/visible
|
||||
let lastSearchHits = null; // for export of search results
|
||||
let currentDocId = null;
|
||||
let currentDocFull = null;
|
||||
const filterState = {}; // per-tab filter state {tab: {godinaMin, godinaMax, vrste:Set, q}}
|
||||
const filterState = {}; // per-tab filter state
|
||||
const unifiedState = { tip:'', izdavatelj:'', q:'', godinaMin:null, godinaMax:null };
|
||||
|
||||
// ─────────────────────────────────────────
|
||||
// Utilities
|
||||
@@ -332,12 +368,108 @@ function setTab(id){
|
||||
$$('.tab').forEach(b => b.classList.toggle('active', b.dataset.tab===id));
|
||||
if(id==='search'){
|
||||
renderSearchView();
|
||||
} else if(id==='svi'){
|
||||
renderUnifiedFilters();
|
||||
loadUnified();
|
||||
} else {
|
||||
renderFilters();
|
||||
applyFilters();
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────
|
||||
// Unified mode (Svi dokumenti)
|
||||
// ─────────────────────────────────────────
|
||||
function renderUnifiedFilters(){
|
||||
const tipSel = TIP_OPTIONS.map(o =>
|
||||
`<option value="${esc(o.v)}" ${unifiedState.tip===o.v?'selected':''}>${esc(o.l)}</option>`).join('');
|
||||
const izdSel = IZDAVATELJ_OPTIONS.map(o =>
|
||||
`<option value="${esc(o.v)}" ${unifiedState.izdavatelj===o.v?'selected':''}>${esc(o.l)}</option>`).join('');
|
||||
$('#filters-wrap').innerHTML = `
|
||||
<div class="filters">
|
||||
<div class="filter-grp">
|
||||
<label>Tip:</label>
|
||||
<select id="uTip" onchange="onUnifiedFilterChange()">${tipSel}</select>
|
||||
</div>
|
||||
<div class="filter-grp">
|
||||
<label>Izdavatelj:</label>
|
||||
<select id="uIzd" onchange="onUnifiedFilterChange()">${izdSel}</select>
|
||||
</div>
|
||||
<div class="filter-grp">
|
||||
<label>Godina:</label>
|
||||
<input type="number" id="uYmin" value="${unifiedState.godinaMin||''}" placeholder="od" min="1990" max="2030" onchange="onUnifiedFilterChange()" style="width:80px">
|
||||
<span>–</span>
|
||||
<input type="number" id="uYmax" value="${unifiedState.godinaMax||''}" placeholder="do" min="1990" max="2030" onchange="onUnifiedFilterChange()" style="width:80px">
|
||||
</div>
|
||||
<div class="filter-grp">
|
||||
<label>Pretraga:</label>
|
||||
<input type="text" id="uQ" value="${esc(unifiedState.q)}" placeholder="naziv / opis / organizacija..." oninput="onUnifiedQDebounce()">
|
||||
</div>
|
||||
<div style="margin-left:auto;color:var(--muted);font-size:0.85rem;" id="filterCount">…</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
let _uQTimer = null;
|
||||
function onUnifiedQDebounce(){
|
||||
clearTimeout(_uQTimer);
|
||||
_uQTimer = setTimeout(() => {
|
||||
unifiedState.q = $('#uQ').value || '';
|
||||
loadUnified();
|
||||
}, 350);
|
||||
}
|
||||
function onUnifiedFilterChange(){
|
||||
unifiedState.tip = $('#uTip').value || '';
|
||||
unifiedState.izdavatelj = $('#uIzd').value || '';
|
||||
unifiedState.godinaMin = parseInt($('#uYmin').value) || null;
|
||||
unifiedState.godinaMax = parseInt($('#uYmax').value) || null;
|
||||
unifiedState.q = $('#uQ').value || '';
|
||||
loadUnified();
|
||||
}
|
||||
|
||||
async function loadUnified(){
|
||||
$('#content').innerHTML = '<div class="loading">Učitavam dokumente…</div>';
|
||||
const params = new URLSearchParams();
|
||||
if(unifiedState.tip) params.set('tip', unifiedState.tip);
|
||||
if(unifiedState.izdavatelj) params.set('izdavatelj', unifiedState.izdavatelj);
|
||||
if(unifiedState.q) params.set('q', unifiedState.q);
|
||||
if(unifiedState.godinaMin) params.set('godina_min', unifiedState.godinaMin);
|
||||
if(unifiedState.godinaMax) params.set('godina_max', unifiedState.godinaMax);
|
||||
params.set('limit', 500);
|
||||
try {
|
||||
const r = await api('/dokumenti/unified?' + params.toString());
|
||||
const j = await r.json();
|
||||
unifiedDocs = j.dokumenti || [];
|
||||
displayDocs = unifiedDocs;
|
||||
$('#filterCount') && ($('#filterCount').textContent = `${unifiedDocs.length} dokumenata`);
|
||||
renderUnifiedContent(unifiedDocs);
|
||||
} catch(e){
|
||||
$('#content').innerHTML = `<div class="empty">⚠️ Greška: ${esc(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderUnifiedContent(docs){
|
||||
if(docs.length === 0){
|
||||
$('#content').innerHTML = `<div class="empty">Nema dokumenata za odabrane filtere.</div>`;
|
||||
return;
|
||||
}
|
||||
const html = docs.map(d => unifiedCardHTML(d)).join('');
|
||||
$('#content').innerHTML = `<div class="grid">${html}</div>`;
|
||||
}
|
||||
|
||||
function unifiedCardHTML(d){
|
||||
const meta = [];
|
||||
if(d.izdavatelj) meta.push(`<span class="chip" style="background:rgba(0,48,135,0.5);color:#aac8ff">${esc(d.izdavatelj)}</span>`);
|
||||
if(d.organizacija) meta.push(`<span style="color:var(--muted)">🏛️ ${esc(d.organizacija.slice(0,28))}${d.organizacija.length>28?'…':''}</span>`);
|
||||
if(d.izdano_datum) meta.push(`<span style="color:var(--muted)">📅 ${fmtDate(d.izdano_datum)}</span>`);
|
||||
else if(d.godina) meta.push(`<span style="color:var(--muted)">📅 ${d.godina}</span>`);
|
||||
return `
|
||||
<div class="card" onclick="openModal(${d.id})">
|
||||
<span class="cardvrsta">${esc(d.tip||d.vrsta||'—')}</span>
|
||||
<div class="cardtitle">${esc(d.title || '(bez naslova)')}</div>
|
||||
<div class="cardmeta" style="flex-wrap:wrap;gap:6px;">${meta.join('')}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────
|
||||
// Filters UI
|
||||
// ─────────────────────────────────────────
|
||||
@@ -507,24 +639,42 @@ function cardHTML(d, yearStyle){
|
||||
// Data load
|
||||
// ─────────────────────────────────────────
|
||||
async function loadAll(){
|
||||
// Load both legacy list (for the 4 vrsta-tabs) and unified facets (for "Svi" badge).
|
||||
$('#content').innerHTML = '<div class="loading">Učitavam dokumente…</div>';
|
||||
try {
|
||||
const r = await api('/dokumenti?limit=1000');
|
||||
const j = await r.json();
|
||||
const [r1, r2] = await Promise.all([
|
||||
api('/dokumenti?limit=1000'),
|
||||
api('/dokumenti/facets'),
|
||||
]);
|
||||
const j = await r1.json();
|
||||
allDocs = (j.dokumenti || []).filter(d => !VRSTE_HIDDEN.includes(d.vrsta));
|
||||
try { unifiedFacets = await r2.json(); } catch(_){ unifiedFacets = null; }
|
||||
updateTabCounts();
|
||||
renderFilters();
|
||||
applyFilters();
|
||||
if(activeTab === 'svi'){
|
||||
renderUnifiedFilters();
|
||||
loadUnified();
|
||||
} else {
|
||||
renderFilters();
|
||||
applyFilters();
|
||||
}
|
||||
} catch(e){
|
||||
$('#content').innerHTML = `<div class="empty">⚠️ Greška pri učitavanju: ${esc(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateTabCounts(){
|
||||
// "svi" tab uses unified facets total; the rest count over allDocs.
|
||||
for(const t of TABS){
|
||||
const old = activeTab; activeTab = t.id;
|
||||
const c = tabDocs().length;
|
||||
activeTab = old;
|
||||
let c = 0;
|
||||
if(t.id === 'svi'){
|
||||
if(unifiedFacets && unifiedFacets.by_tip){
|
||||
c = unifiedFacets.by_tip.reduce((s,x) => s + (x.broj||0), 0);
|
||||
} else { c = '—'; }
|
||||
} else {
|
||||
const old = activeTab; activeTab = t.id;
|
||||
c = tabDocs().length;
|
||||
activeTab = old;
|
||||
}
|
||||
const el = document.getElementById('cnt-'+t.id);
|
||||
if(el) el.textContent = c;
|
||||
}
|
||||
@@ -658,6 +808,31 @@ function setModalTab(t){
|
||||
${d.izvor_url ? `<div class="info-row"><div class="lab">Izvor URL</div><div class="val"><a href="${esc(d.izvor_url)}" target="_blank">${esc(d.izvor_url)}</a></div></div>` : ''}
|
||||
<div class="info-row"><div class="lab">RAG chunks</div><div class="val">${currentDocFull.chunks_count || 0}</div></div>
|
||||
`;
|
||||
} else if(t === 'pdf'){
|
||||
// Inline PDF viewer — try server stream, fall back to external pdf_url, then "open in new tab".
|
||||
const localPdf = `${API}/dokumenti/${d.id}/pdf`;
|
||||
const fallbackPdf = `${API_DIRECT}/dokumenti/${d.id}/pdf`;
|
||||
const externalPdf = d.pdf_url || d.izvor_url || null;
|
||||
$('#mBody').innerHTML = `
|
||||
<div style="margin-bottom:8px;display:flex;gap:8px;flex-wrap:wrap;">
|
||||
<a class="btn gold sm" target="_blank" rel="noopener" href="${esc(localPdf)}">⤴ Otvori u novom tabu</a>
|
||||
${externalPdf ? `<a class="btn secondary sm" target="_blank" rel="noopener" href="${esc(externalPdf)}">🔗 Originalni izvor</a>` : ''}
|
||||
</div>
|
||||
<iframe class="pdf-frame" id="pdfFrame" src="${esc(localPdf)}"
|
||||
onerror="this.src='${esc(fallbackPdf)}'"></iframe>
|
||||
<div id="pdfFallback" style="display:none;" class="pdf-fallback">
|
||||
PDF nije dostupan u inline pregledniku.
|
||||
${externalPdf ? `Pokušaj <a href="${esc(externalPdf)}" target="_blank">originalni izvor</a>.` : ''}
|
||||
</div>
|
||||
`;
|
||||
// Detect 404 on iframe load — best-effort timeout
|
||||
setTimeout(()=>{
|
||||
try {
|
||||
const fr = $('#pdfFrame');
|
||||
if(!fr || !fr.contentWindow) return;
|
||||
// Same-origin? May read; cross-origin we can't introspect — leave as-is.
|
||||
} catch(_){}
|
||||
}, 1500);
|
||||
} else if(t === 'search'){
|
||||
$('#mBody').innerHTML = `
|
||||
<div class="inline-search">
|
||||
@@ -721,10 +896,15 @@ function exportXLSX(){
|
||||
return;
|
||||
}
|
||||
const isSearch = activeTab === 'search';
|
||||
const isUnified = activeTab === 'svi';
|
||||
const rows = isSearch ? (lastSearchHits || []).map(h => ({
|
||||
id:h.id, title:h.title, vrsta:h.vrsta, godina:h.godina,
|
||||
izdano_datum:h.izdano_datum, izvor_url:h.izvor_url,
|
||||
excerpt:(h.excerpt||'').replace(/\s+/g,' ').slice(0,300)
|
||||
})) : isUnified ? displayDocs.map(d => ({
|
||||
id:d.id, title:d.title, tip:d.tip, izdavatelj:d.izdavatelj,
|
||||
vrsta:d.vrsta, godina:d.godina, organizacija:d.organizacija,
|
||||
izdano_datum:d.izdano_datum, izvor_url:d.izvor_url, chars:d.chars
|
||||
})) : displayDocs.map(d => ({
|
||||
id:d.id, title:d.title, vrsta:d.vrsta, godina:d.godina,
|
||||
organizacija:d.organizacija, izdano_datum:d.izdano_datum,
|
||||
|
||||
Reference in New Issue
Block a user