1d02c0897d
- pgz nav now includes /erp/full, /crm/v2, /admin/users, /dokumenti
- 4 dokumenti endpoints: list, godišnjaci/list, godišnjak/{godina} PDF, detail
- 18 godišnjaka u pgz_sport.dokumenti (2006-2024) with savez_id=333
- PGŽ filter helpers (window._pgz_filter_priority, togglePGZFilter)
- navItemClick handler for nav items with href
752 lines
33 KiB
HTML
752 lines
33 KiB
HTML
<!DOCTYPE html>
|
||
<!--
|
||
PGŽ Sport — Dokumenti UI v1.0
|
||
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.
|
||
-->
|
||
<html lang="hr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Dokumenti — PGŽ Sport</title>
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<link rel="icon" href="data:,">
|
||
<style>
|
||
:root { --bg0:#08090e; --bg1:#11141d; --bg2:#1a1f2c; --bg3:#232a3d;
|
||
--txt:#e6e9ef; --muted:#7a8294; --line:#1a1f2c;
|
||
--pgz-blue:#003087; --pgz-blue-l:#0040b8; --pgz-gold:#F4C430;
|
||
--green:#1a8754; --red:#dc3545; --orange:#fd7e14; --cyan:#5fa8d3; }
|
||
* { box-sizing:border-box; margin:0; padding:0; }
|
||
body { font-family:'Inter',system-ui,sans-serif; background:var(--bg0); color:var(--txt);
|
||
padding:20px; line-height:1.5; min-height:100vh; }
|
||
a { color:var(--cyan); }
|
||
h1 { color:var(--pgz-gold); margin-bottom:4px; font-size:1.6rem; }
|
||
h2 { color:var(--pgz-gold); margin:0 0 10px 0; font-size:1.1rem; }
|
||
h3 { color:var(--txt); margin:0 0 6px 0; font-size:0.98rem; }
|
||
.sub { color:var(--muted); margin-bottom:18px; font-size:0.92rem; }
|
||
|
||
/* Top toolbar — global search + export */
|
||
.top-toolbar { display:flex; gap:10px; flex-wrap:wrap; align-items:center; margin-bottom:14px; }
|
||
.top-toolbar .global-search { flex:1; min-width:280px; position:relative; }
|
||
.top-toolbar .global-search input {
|
||
width:100%; padding:11px 14px 11px 38px; border-radius:8px; border:1px solid #2a3144;
|
||
background:var(--bg1); color:var(--txt); font-size:0.95rem;
|
||
}
|
||
.top-toolbar .global-search::before {
|
||
content:"🔎"; position:absolute; left:12px; top:9px; opacity:0.7;
|
||
}
|
||
.btn { background:var(--pgz-blue); color:white; border:none; padding:10px 16px;
|
||
border-radius:6px; cursor:pointer; font-weight:500; font-size:0.92rem;
|
||
transition:background 0.15s; }
|
||
.btn:hover { background:var(--pgz-blue-l); }
|
||
.btn.secondary { background:var(--bg2); color:var(--txt); border:1px solid #2a3144; }
|
||
.btn.secondary:hover { background:var(--bg3); }
|
||
.btn.gold { background:var(--pgz-gold); color:#000; }
|
||
.btn.gold:hover { background:#ffe066; }
|
||
.btn.sm { padding:6px 11px; font-size:0.84rem; }
|
||
.btn:disabled { opacity:0.4; cursor:not-allowed; }
|
||
|
||
/* Tabbar */
|
||
.tabbar { display:flex; gap:2px; border-bottom:2px solid #2a3144; margin-bottom:14px;
|
||
overflow-x:auto; -webkit-overflow-scrolling:touch; }
|
||
.tab { padding:11px 18px; cursor:pointer; color:var(--muted); border:none; background:none;
|
||
font-size:0.95rem; font-weight:500; border-bottom:3px solid transparent;
|
||
margin-bottom:-2px; white-space:nowrap; transition:all 0.15s; }
|
||
.tab:hover { color:var(--txt); }
|
||
.tab.active { color:var(--pgz-gold); border-bottom-color:var(--pgz-gold); }
|
||
.tab .count { color:var(--muted); font-size:0.78rem; margin-left:6px;
|
||
background:var(--bg2); padding:1px 7px; border-radius:9px; }
|
||
.tab.active .count { color:var(--pgz-gold); }
|
||
|
||
/* Filters bar */
|
||
.filters { display:flex; gap:12px; flex-wrap:wrap; align-items:center;
|
||
background:var(--bg1); padding:12px 14px; border-radius:8px; margin-bottom:14px; }
|
||
.filter-grp { display:flex; gap:8px; align-items:center; font-size:0.88rem; }
|
||
.filter-grp label { color:var(--muted); }
|
||
.filter-grp input[type=number], .filter-grp input[type=text], .filter-grp select {
|
||
background:var(--bg2); color:var(--txt); border:1px solid #2a3144; padding:7px 9px;
|
||
border-radius:5px; font-size:0.88rem; min-width:90px;
|
||
}
|
||
.filter-grp input[type=text] { min-width:200px; }
|
||
.multiselect { position:relative; }
|
||
.multiselect-btn {
|
||
background:var(--bg2); border:1px solid #2a3144; padding:7px 10px; border-radius:5px;
|
||
color:var(--txt); cursor:pointer; min-width:160px; text-align:left; font-size:0.88rem;
|
||
}
|
||
.multiselect-pop {
|
||
position:absolute; top:100%; left:0; margin-top:4px; background:var(--bg2);
|
||
border:1px solid #2a3144; border-radius:6px; padding:6px; z-index:50;
|
||
max-height:240px; overflow-y:auto; min-width:200px; display:none;
|
||
box-shadow:0 4px 24px rgba(0,0,0,0.5);
|
||
}
|
||
.multiselect-pop.open { display:block; }
|
||
.multiselect-pop label {
|
||
display:flex; align-items:center; gap:8px; padding:5px 7px; border-radius:4px;
|
||
cursor:pointer; font-size:0.88rem;
|
||
}
|
||
.multiselect-pop label:hover { background:var(--bg3); }
|
||
|
||
/* Cards grid */
|
||
.grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(260px, 1fr));
|
||
gap:14px; }
|
||
.card {
|
||
background:var(--bg1); border:1px solid #2a3144; border-radius:10px; padding:14px;
|
||
cursor:pointer; transition:transform 0.15s, border-color 0.15s, box-shadow 0.15s;
|
||
display:flex; flex-direction:column; gap:8px; min-height:160px;
|
||
}
|
||
.card:hover {
|
||
transform:translateY(-2px); border-color:var(--pgz-gold);
|
||
box-shadow:0 6px 24px rgba(244,196,48,0.12);
|
||
}
|
||
.card .cardyear {
|
||
font-size:1.7rem; font-weight:700; color:var(--pgz-gold); line-height:1; letter-spacing:-1px;
|
||
}
|
||
.card .cardvrsta {
|
||
display:inline-block; font-size:0.72rem; padding:2px 8px; border-radius:9px;
|
||
background:rgba(0,48,135,0.5); color:#aac8ff; align-self:flex-start; text-transform:uppercase;
|
||
letter-spacing:0.5px; font-weight:600;
|
||
}
|
||
.card .cardtitle {
|
||
font-size:0.95rem; font-weight:600; color:var(--txt); flex:1; line-height:1.3;
|
||
}
|
||
.card .cardmeta { font-size:0.78rem; color:var(--muted); display:flex; gap:10px; }
|
||
.card .cardmeta span { display:flex; align-items:center; gap:3px; }
|
||
|
||
/* Loading & empty */
|
||
.empty { padding:60px 20px; text-align:center; color:var(--muted); font-size:0.95rem; }
|
||
.loading { padding:30px; text-align:center; color:var(--muted); }
|
||
.loading::before {
|
||
content:""; display:inline-block; width:18px; height:18px;
|
||
border:2px solid var(--bg3); border-top-color:var(--pgz-gold);
|
||
border-radius:50%; animation:spin 0.8s linear infinite; margin-right:8px;
|
||
vertical-align:middle;
|
||
}
|
||
@keyframes spin { to { transform:rotate(360deg); } }
|
||
|
||
/* Search results table */
|
||
.search-results { background:var(--bg1); border-radius:8px; overflow:hidden; }
|
||
.search-results .res-row {
|
||
padding:14px 18px; border-bottom:1px solid var(--line); cursor:pointer;
|
||
transition:background 0.15s;
|
||
}
|
||
.search-results .res-row:hover { background:var(--bg2); }
|
||
.search-results .res-row:last-child { border-bottom:none; }
|
||
.search-results .res-title { font-weight:600; color:var(--pgz-gold); }
|
||
.search-results .res-meta { font-size:0.8rem; color:var(--muted); margin:3px 0 6px 0; }
|
||
.search-results .res-excerpt {
|
||
font-size:0.88rem; color:#cdd2dc; background:var(--bg2); padding:8px 11px;
|
||
border-radius:5px; border-left:3px solid var(--cyan);
|
||
}
|
||
.search-results mark {
|
||
background:var(--pgz-gold); color:#000; padding:1px 3px; border-radius:3px;
|
||
}
|
||
|
||
/* Modal */
|
||
.modal-bg {
|
||
position:fixed; inset:0; background:rgba(0,0,0,0.7); backdrop-filter:blur(4px);
|
||
z-index:100; display:none; align-items:flex-start; justify-content:center;
|
||
padding:40px 20px; overflow-y:auto;
|
||
}
|
||
.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;
|
||
box-shadow:0 12px 60px rgba(0,0,0,0.6);
|
||
}
|
||
.modal-h {
|
||
padding:18px 22px; border-bottom:1px solid var(--line); display:flex;
|
||
align-items:flex-start; justify-content:space-between; gap:12px;
|
||
}
|
||
.modal-h h2 { color:var(--pgz-gold); font-size:1.15rem; flex:1; }
|
||
.modal-h .close {
|
||
background:none; border:none; color:var(--muted); font-size:1.5rem; cursor:pointer;
|
||
padding:0 4px; line-height:1;
|
||
}
|
||
.modal-h .close:hover { color:var(--txt); }
|
||
.modal-tabs { display:flex; gap:0; padding:0 22px; border-bottom:1px solid var(--line); }
|
||
.modal-tab {
|
||
padding:9px 14px; cursor:pointer; color:var(--muted); border:none; background:none;
|
||
font-size:0.9rem; border-bottom:2px solid transparent; margin-bottom:-1px;
|
||
}
|
||
.modal-tab.active { color:var(--pgz-gold); border-bottom-color:var(--pgz-gold); }
|
||
.modal-body { padding:18px 22px; max-height:60vh; overflow-y:auto; }
|
||
.modal-actions {
|
||
padding:14px 22px; border-top:1px solid var(--line); display:flex; gap:10px; flex-wrap:wrap;
|
||
}
|
||
.info-row {
|
||
display:flex; gap:14px; padding:6px 0; border-bottom:1px dashed #1f2535; font-size:0.92rem;
|
||
}
|
||
.info-row:last-child { border-bottom:none; }
|
||
.info-row .lab { color:var(--muted); min-width:130px; }
|
||
.info-row .val { flex:1; word-break:break-word; }
|
||
.chip {
|
||
display:inline-block; background:var(--bg2); padding:2px 8px; border-radius:9px;
|
||
font-size:0.78rem; margin:0 4px 4px 0;
|
||
}
|
||
.chunk {
|
||
background:var(--bg2); padding:10px 14px; border-radius:6px; margin-bottom:10px;
|
||
border-left:3px solid var(--cyan); font-size:0.88rem; line-height:1.55;
|
||
}
|
||
.chunk .chunk-meta { font-size:0.75rem; color:var(--muted); margin-bottom:5px; }
|
||
|
||
/* Inline scoped search */
|
||
.inline-search {
|
||
background:var(--bg2); padding:12px 14px; border-radius:6px; margin-bottom:14px;
|
||
display:flex; gap:8px; align-items:center;
|
||
}
|
||
.inline-search input {
|
||
flex:1; background:var(--bg1); color:var(--txt); border:1px solid #2a3144;
|
||
padding:7px 11px; border-radius:5px; font-size:0.9rem;
|
||
}
|
||
|
||
/* Sidebar offset */
|
||
body.pgz-has-sb { padding-left:280px; }
|
||
body.pgz-has-sb.pgz-sb-col { padding-left:80px; }
|
||
@media (max-width:900px) {
|
||
body.pgz-has-sb { padding-left:20px; }
|
||
body.pgz-has-sb.pgz-sb-col { padding-left:20px; }
|
||
}
|
||
</style>
|
||
<link rel="stylesheet" href="/static/shared/sidebar.css">
|
||
<script src="/static/shared/sidebar.js" defer data-active="dokumentilib"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js" defer></script>
|
||
</head>
|
||
<body>
|
||
|
||
<h1>📚 Dokumenti</h1>
|
||
<div class="sub">Knjižnica svih sportskih publikacija PGŽ — godišnjaci ZSPGZ, pravilnici, programi, izvještaji. Klik na dokument otvara PDF + RAG citate.</div>
|
||
|
||
<div class="top-toolbar">
|
||
<div class="global-search">
|
||
<input id="globalQ" type="search" placeholder="Pretraži kroz cjelokupni sadržaj svih dokumenata (npr. „Andrijaševic", „mladi", „proračun"…)">
|
||
</div>
|
||
<button class="btn" onclick="globalSearch()">Pretraži</button>
|
||
<button class="btn secondary" id="btnExport" onclick="exportXLSX()">📥 XLSX</button>
|
||
</div>
|
||
|
||
<div class="tabbar" id="tabbar"></div>
|
||
|
||
<div id="filters-wrap"></div>
|
||
|
||
<div id="content"></div>
|
||
|
||
<!-- Drill-down modal -->
|
||
<div class="modal-bg" id="modal" onclick="if(event.target===this) closeModal()">
|
||
<div class="modal" id="modalInner">
|
||
<div class="modal-h">
|
||
<h2 id="mTitle">…</h2>
|
||
<button class="close" onclick="closeModal()">×</button>
|
||
</div>
|
||
<div class="modal-tabs">
|
||
<button class="modal-tab active" data-mtab="info" onclick="setModalTab('info')">ℹ️ Info</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>
|
||
<div class="modal-body" id="mBody">…</div>
|
||
<div class="modal-actions" id="mActions"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
'use strict';
|
||
|
||
// ─────────────────────────────────────────
|
||
// Tab definitions (vrste mapping)
|
||
// ─────────────────────────────────────────
|
||
const TABS = [
|
||
{ id:'godisnjaci', label:'📅 Godišnjaci ZSPGZ',
|
||
vrste:['godisnjak'], view:'cards-year' },
|
||
{ id:'publikacije', label:'📰 Publikacije',
|
||
vrste:['program','plan','strategija','izvjestaj','raspodjela','erasmus'], view:'cards' },
|
||
{ id:'pravilnici', label:'📜 Pravilnici',
|
||
vrste:['pravilnik','statut','odluka','zakon'], view:'cards' },
|
||
{ id:'javni', label:'📢 Javni pozivi',
|
||
vrste:['manifestacija'], view:'cards', titleLike:['javni','natje','poziv','potpor'] },
|
||
{ id:'ostalo', label:'📂 Ostalo',
|
||
vrsteExclude:['godisnjak','program','plan','strategija','izvjestaj','raspodjela','erasmus',
|
||
'pravilnik','statut','odluka','zakon','manifestacija',
|
||
'corpus','corpus_v2','corpus_v3','novost_savez','audit','godisnjak_facts'],
|
||
view:'cards' },
|
||
];
|
||
|
||
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
|
||
const API_DIRECT = '/api/v2';
|
||
|
||
// ─────────────────────────────────────────
|
||
// State
|
||
// ─────────────────────────────────────────
|
||
let activeTab = 'godisnjaci';
|
||
let allDocs = []; // all loaded docs (cached after first fetch)
|
||
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}}
|
||
|
||
// ─────────────────────────────────────────
|
||
// Utilities
|
||
// ─────────────────────────────────────────
|
||
const $ = (s, root) => (root||document).querySelector(s);
|
||
const $$ = (s, root) => Array.from((root||document).querySelectorAll(s));
|
||
const esc = s => String(s==null?'':s).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
|
||
|
||
function fmtDate(d){
|
||
if(!d) return '—';
|
||
try { return new Date(d).toLocaleDateString('hr-HR'); } catch(e){ return d; }
|
||
}
|
||
|
||
async function api(path, opts){
|
||
// Try /sport/api/v2 first, fall back to /api/v2 (same-origin direct)
|
||
try {
|
||
const r = await fetch(API + path, opts);
|
||
if(r.ok || r.status === 404) return r;
|
||
} catch(e) {}
|
||
return await fetch(API_DIRECT + path, opts);
|
||
}
|
||
|
||
function highlightTerm(text, q){
|
||
if(!text || !q) return esc(text||'');
|
||
const safe = esc(text);
|
||
const re = new RegExp('(' + q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
|
||
return safe.replace(re, '<mark>$1</mark>');
|
||
}
|
||
|
||
// ─────────────────────────────────────────
|
||
// Initial render
|
||
// ─────────────────────────────────────────
|
||
function renderTabs(){
|
||
const html = TABS.map(t => `
|
||
<button class="tab ${t.id===activeTab?'active':''}" data-tab="${t.id}" onclick="setTab('${t.id}')">
|
||
${t.label}<span class="count" id="cnt-${t.id}">…</span>
|
||
</button>`).join('') +
|
||
`<button class="tab" data-tab="search" id="tab-search" style="display:none" onclick="setTab('search')">
|
||
🔎 Rezultati pretrage<span class="count" id="cnt-search">0</span>
|
||
</button>`;
|
||
$('#tabbar').innerHTML = html;
|
||
}
|
||
|
||
function setTab(id){
|
||
activeTab = id;
|
||
$$('.tab').forEach(b => b.classList.toggle('active', b.dataset.tab===id));
|
||
if(id==='search'){
|
||
renderSearchView();
|
||
} else {
|
||
renderFilters();
|
||
applyFilters();
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────
|
||
// Filters UI
|
||
// ─────────────────────────────────────────
|
||
function getTabConfig(){ return TABS.find(t => t.id===activeTab); }
|
||
|
||
function tabDocs(){
|
||
const cfg = getTabConfig();
|
||
if(!cfg) return [];
|
||
return allDocs.filter(d => {
|
||
if(VRSTE_HIDDEN.includes(d.vrsta)) return false;
|
||
if(cfg.vrste && !cfg.vrste.includes(d.vrsta)) return false;
|
||
if(cfg.vrsteExclude && cfg.vrsteExclude.includes(d.vrsta)) return false;
|
||
if(cfg.titleLike){
|
||
const t = (d.title||'').toLowerCase();
|
||
const desc = (d.kratak_opis||'').toLowerCase();
|
||
if(!cfg.titleLike.some(k => t.includes(k) || desc.includes(k))) return false;
|
||
}
|
||
return true;
|
||
});
|
||
}
|
||
|
||
function renderFilters(){
|
||
const docs = tabDocs();
|
||
const years = docs.map(d => d.godina).filter(Boolean).sort((a,b)=>a-b);
|
||
const minY = years[0] || 2000;
|
||
const maxY = years[years.length-1] || 2026;
|
||
const vrste = [...new Set(docs.map(d => d.vrsta).filter(Boolean))].sort();
|
||
|
||
const fs = filterState[activeTab] = filterState[activeTab] || {
|
||
godinaMin:minY, godinaMax:maxY, vrste:new Set(vrste), q:''
|
||
};
|
||
// re-clamp on first render
|
||
if(fs.godinaMin < minY) fs.godinaMin = minY;
|
||
if(fs.godinaMax > maxY) fs.godinaMax = maxY;
|
||
|
||
$('#filters-wrap').innerHTML = `
|
||
<div class="filters">
|
||
<div class="filter-grp">
|
||
<label>Godina:</label>
|
||
<input type="number" id="fyMin" value="${fs.godinaMin}" min="${minY}" max="${maxY}" onchange="onFilterChange()">
|
||
<span>–</span>
|
||
<input type="number" id="fyMax" value="${fs.godinaMax}" min="${minY}" max="${maxY}" onchange="onFilterChange()">
|
||
</div>
|
||
<div class="filter-grp">
|
||
<label>Vrsta:</label>
|
||
<div class="multiselect">
|
||
<button class="multiselect-btn" id="msBtn" onclick="toggleMS(event)">${msLabel(fs.vrste, vrste)}</button>
|
||
<div class="multiselect-pop" id="msPop">
|
||
${vrste.map(v => `
|
||
<label><input type="checkbox" data-vrsta="${esc(v)}" ${fs.vrste.has(v)?'checked':''} onchange="onMSChange()"> ${esc(v)}</label>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="filter-grp">
|
||
<label>U tabu:</label>
|
||
<input type="text" id="fq" value="${esc(fs.q)}" placeholder="filter po nazivu/opisu..." oninput="onFilterChange()">
|
||
</div>
|
||
<div style="margin-left:auto;color:var(--muted);font-size:0.85rem;" id="filterCount">…</div>
|
||
</div>`;
|
||
}
|
||
|
||
function msLabel(setVal, all){
|
||
if(setVal.size === 0) return 'Ništa odabrano';
|
||
if(setVal.size === all.length) return `Sve vrste (${all.length})`;
|
||
return `${setVal.size} odabrano`;
|
||
}
|
||
|
||
function toggleMS(ev){
|
||
ev.stopPropagation();
|
||
const pop = $('#msPop');
|
||
const isOpen = pop.classList.toggle('open');
|
||
if(isOpen){
|
||
setTimeout(() => {
|
||
const closer = (e) => {
|
||
if(!pop.contains(e.target) && e.target.id !== 'msBtn'){
|
||
pop.classList.remove('open');
|
||
document.removeEventListener('click', closer);
|
||
}
|
||
};
|
||
document.addEventListener('click', closer);
|
||
}, 50);
|
||
}
|
||
}
|
||
function onMSChange(){
|
||
const fs = filterState[activeTab];
|
||
fs.vrste = new Set($$('#msPop input:checked').map(i => i.dataset.vrsta));
|
||
const docs = tabDocs();
|
||
const allVrste = [...new Set(docs.map(d => d.vrsta))];
|
||
$('#msBtn').textContent = msLabel(fs.vrste, allVrste);
|
||
applyFilters();
|
||
}
|
||
function onFilterChange(){
|
||
const fs = filterState[activeTab];
|
||
fs.godinaMin = parseInt($('#fyMin').value) || fs.godinaMin;
|
||
fs.godinaMax = parseInt($('#fyMax').value) || fs.godinaMax;
|
||
fs.q = $('#fq').value || '';
|
||
applyFilters();
|
||
}
|
||
|
||
// ─────────────────────────────────────────
|
||
// Apply filters → render content
|
||
// ─────────────────────────────────────────
|
||
function applyFilters(){
|
||
const cfg = getTabConfig();
|
||
const fs = filterState[activeTab];
|
||
let docs = tabDocs();
|
||
if(fs){
|
||
docs = docs.filter(d => {
|
||
if(d.godina != null){
|
||
if(d.godina < fs.godinaMin || d.godina > fs.godinaMax) return false;
|
||
} else {
|
||
// no year — only show if range covers full span (i.e., user hasn't narrowed)
|
||
}
|
||
if(fs.vrste && fs.vrste.size > 0 && !fs.vrste.has(d.vrsta)) return false;
|
||
if(fs.q){
|
||
const ql = fs.q.toLowerCase();
|
||
const hit = (d.title||'').toLowerCase().includes(ql) ||
|
||
(d.kratak_opis||'').toLowerCase().includes(ql) ||
|
||
(d.organizacija||'').toLowerCase().includes(ql);
|
||
if(!hit) return false;
|
||
}
|
||
return true;
|
||
});
|
||
}
|
||
// Sort: godisnjaci by year DESC; ostalo by izdano_datum DESC, then godina DESC
|
||
if(cfg && cfg.view === 'cards-year'){
|
||
docs.sort((a,b)=> (b.godina||0) - (a.godina||0));
|
||
} else {
|
||
docs.sort((a,b) => {
|
||
const ay = a.godina || 0, by = b.godina || 0;
|
||
if(ay !== by) return by - ay;
|
||
return (a.title||'').localeCompare(b.title||'', 'hr');
|
||
});
|
||
}
|
||
displayDocs = docs;
|
||
$('#filterCount') && ($('#filterCount').textContent = `${docs.length} dokumenata`);
|
||
renderContent(docs);
|
||
}
|
||
|
||
function renderContent(docs){
|
||
if(docs.length === 0){
|
||
$('#content').innerHTML = `<div class="empty">Nema dokumenata za odabrane filtere.</div>`;
|
||
return;
|
||
}
|
||
const cfg = getTabConfig();
|
||
const html = docs.map(d => cardHTML(d, cfg && cfg.view === 'cards-year')).join('');
|
||
$('#content').innerHTML = `<div class="grid">${html}</div>`;
|
||
}
|
||
|
||
function cardHTML(d, yearStyle){
|
||
const year = d.godina ? `<div class="cardyear">${d.godina}</div>` : '';
|
||
const meta = [];
|
||
if(d.organizacija) meta.push(`<span>🏛️ ${esc(d.organizacija.slice(0,30))}${d.organizacija.length>30?'…':''}</span>`);
|
||
if(d.izdano_datum) meta.push(`<span>📅 ${fmtDate(d.izdano_datum)}</span>`);
|
||
if(d.chars) meta.push(`<span>📝 ${(d.chars/1000).toFixed(0)}K znakova</span>`);
|
||
return `
|
||
<div class="card" onclick="openModal(${d.id})">
|
||
${yearStyle ? year : ''}
|
||
<span class="cardvrsta">${esc(d.vrsta||'—')}</span>
|
||
<div class="cardtitle">${esc(d.title || '(bez naslova)')}</div>
|
||
<div class="cardmeta">${meta.join('')}</div>
|
||
</div>`;
|
||
}
|
||
|
||
// ─────────────────────────────────────────
|
||
// Data load
|
||
// ─────────────────────────────────────────
|
||
async function loadAll(){
|
||
$('#content').innerHTML = '<div class="loading">Učitavam dokumente…</div>';
|
||
try {
|
||
const r = await api('/dokumenti?limit=1000');
|
||
const j = await r.json();
|
||
allDocs = (j.dokumenti || []).filter(d => !VRSTE_HIDDEN.includes(d.vrsta));
|
||
updateTabCounts();
|
||
renderFilters();
|
||
applyFilters();
|
||
} catch(e){
|
||
$('#content').innerHTML = `<div class="empty">⚠️ Greška pri učitavanju: ${esc(e.message)}</div>`;
|
||
}
|
||
}
|
||
|
||
function updateTabCounts(){
|
||
for(const t of TABS){
|
||
const old = activeTab; activeTab = t.id;
|
||
const c = tabDocs().length;
|
||
activeTab = old;
|
||
const el = document.getElementById('cnt-'+t.id);
|
||
if(el) el.textContent = c;
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────
|
||
// Global search
|
||
// ─────────────────────────────────────────
|
||
async function globalSearch(){
|
||
const q = $('#globalQ').value.trim();
|
||
if(!q || q.length < 2){
|
||
alert('Upiši minimalno 2 znaka za pretragu.');
|
||
return;
|
||
}
|
||
$('#tab-search').style.display = '';
|
||
setTab('search');
|
||
$('#content').innerHTML = '<div class="loading">Pretražujem cjelokupni sadržaj…</div>';
|
||
$('#filters-wrap').innerHTML = `
|
||
<div class="filters">
|
||
<div class="filter-grp">
|
||
<strong>🔎 Pretraga: </strong><span style="color:var(--pgz-gold)">"${esc(q)}"</span>
|
||
</div>
|
||
<div style="margin-left:auto;color:var(--muted);font-size:0.85rem;" id="searchInfo">…</div>
|
||
</div>`;
|
||
try {
|
||
const r = await api('/dokumenti/search/q?q=' + encodeURIComponent(q) + '&limit=80');
|
||
const j = await r.json();
|
||
lastSearchHits = j.rezultati || [];
|
||
$('#cnt-search').textContent = lastSearchHits.length;
|
||
$('#searchInfo').textContent = `${lastSearchHits.length} pogodaka`;
|
||
renderSearchResults(lastSearchHits, q);
|
||
} catch(e){
|
||
$('#content').innerHTML = `<div class="empty">⚠️ Pretraga neuspjela: ${esc(e.message)}</div>`;
|
||
}
|
||
}
|
||
|
||
function renderSearchView(){
|
||
if(!lastSearchHits){
|
||
$('#content').innerHTML = '<div class="empty">Upiši upit u tražilicu iznad i klikni „Pretraži".</div>';
|
||
$('#filters-wrap').innerHTML = '';
|
||
return;
|
||
}
|
||
const q = $('#globalQ').value.trim();
|
||
renderSearchResults(lastSearchHits, q);
|
||
}
|
||
|
||
function renderSearchResults(hits, q){
|
||
if(hits.length === 0){
|
||
$('#content').innerHTML = `<div class="empty">Nema pogodaka za "${esc(q)}".</div>`;
|
||
return;
|
||
}
|
||
const html = hits.map(h => `
|
||
<div class="res-row" onclick="openModal(${h.id})">
|
||
<div class="res-title">${esc(h.title || '(bez naslova)')}</div>
|
||
<div class="res-meta">
|
||
<span class="chip">${esc(h.vrsta||'—')}</span>
|
||
${h.godina ? '<span class="chip">'+h.godina+'</span>' : ''}
|
||
${h.izdano_datum ? '<span class="chip">'+fmtDate(h.izdano_datum)+'</span>' : ''}
|
||
</div>
|
||
<div class="res-excerpt">${highlightTerm(h.excerpt||'', q)}</div>
|
||
</div>`).join('');
|
||
$('#content').innerHTML = `<div class="search-results">${html}</div>`;
|
||
}
|
||
|
||
// ─────────────────────────────────────────
|
||
// Modal — drill-down
|
||
// ─────────────────────────────────────────
|
||
async function openModal(id){
|
||
currentDocId = id;
|
||
currentDocFull = null;
|
||
$('#modal').classList.add('open');
|
||
$('#mTitle').textContent = 'Učitavam…';
|
||
$('#mBody').innerHTML = '<div class="loading">Učitavam detalje…</div>';
|
||
$('#mActions').innerHTML = '';
|
||
setModalTab('info');
|
||
try {
|
||
const r = await api(`/dokumenti/${id}/full`);
|
||
if(!r.ok){
|
||
// Fallback to basic detail
|
||
const r2 = await api(`/dokumenti/${id}`);
|
||
const j2 = await r2.json();
|
||
currentDocFull = { dokument: { ...j2, naziv: j2.title }, chunks: [], chunks_count: 0 };
|
||
} else {
|
||
currentDocFull = await r.json();
|
||
}
|
||
const d = currentDocFull.dokument;
|
||
$('#mTitle').textContent = d.naziv || d.title || '(bez naslova)';
|
||
renderModalActions(d);
|
||
setModalTab('info');
|
||
} catch(e){
|
||
$('#mBody').innerHTML = `<div class="empty">⚠️ Greška: ${esc(e.message)}</div>`;
|
||
}
|
||
}
|
||
|
||
function closeModal(){
|
||
$('#modal').classList.remove('open');
|
||
currentDocId = null; currentDocFull = null;
|
||
}
|
||
document.addEventListener('keydown', e => { if(e.key === 'Escape') closeModal(); });
|
||
|
||
function renderModalActions(d){
|
||
const pdfUrl = d.pdf_url || `${API}/dokumenti/${d.id}/pdf`;
|
||
const fallbackPdf = `${API_DIRECT}/dokumenti/${d.id}/pdf`;
|
||
const txtUrl = `${API}/dokumenti/${d.id}/text`;
|
||
$('#mActions').innerHTML = `
|
||
<a class="btn gold" target="_blank" rel="noopener" href="${esc(pdfUrl)}"
|
||
onerror="this.href='${esc(fallbackPdf)}'">📄 Otvori PDF</a>
|
||
<a class="btn secondary" target="_blank" rel="noopener" href="${esc(txtUrl)}">📝 Plain text</a>
|
||
${d.izvor_url ? `<a class="btn secondary" target="_blank" rel="noopener" href="${esc(d.izvor_url)}">🔗 Izvor</a>` : ''}
|
||
<button class="btn secondary" style="margin-left:auto" onclick="closeModal()">Zatvori</button>`;
|
||
}
|
||
|
||
function setModalTab(t){
|
||
$$('.modal-tab').forEach(b => b.classList.toggle('active', b.dataset.mtab === t));
|
||
if(!currentDocFull){ return; }
|
||
const d = currentDocFull.dokument;
|
||
if(t === 'info'){
|
||
const kr = Array.isArray(d.kljucne_rijeci) ? d.kljucne_rijeci :
|
||
(typeof d.kljucne_rijeci === 'string' ? d.kljucne_rijeci.split(/[,;]/).map(s=>s.trim()).filter(Boolean) : []);
|
||
$('#mBody').innerHTML = `
|
||
<div class="info-row"><div class="lab">Naslov</div><div class="val">${esc(d.naziv||d.title||'—')}</div></div>
|
||
${d.kratak_opis ? `<div class="info-row"><div class="lab">Opis</div><div class="val">${esc(d.kratak_opis)}</div></div>` : ''}
|
||
<div class="info-row"><div class="lab">Vrsta</div><div class="val"><span class="chip">${esc(d.vrsta||'—')}</span></div></div>
|
||
${d.razina ? `<div class="info-row"><div class="lab">Razina</div><div class="val">${esc(d.razina)}</div></div>` : ''}
|
||
${d.organizacija ? `<div class="info-row"><div class="lab">Organizacija</div><div class="val">${esc(d.organizacija)}</div></div>` : ''}
|
||
${d.sport ? `<div class="info-row"><div class="lab">Sport</div><div class="val">${esc(d.sport)}</div></div>` : ''}
|
||
${d.godina ? `<div class="info-row"><div class="lab">Godina</div><div class="val">${esc(d.godina)}</div></div>` : ''}
|
||
${d.izdano_datum ? `<div class="info-row"><div class="lab">Izdano</div><div class="val">${fmtDate(d.izdano_datum)}</div></div>` : ''}
|
||
${d.sluzbeni_glasnik ? `<div class="info-row"><div class="lab">Službeni glasnik</div><div class="val">${esc(d.sluzbeni_glasnik)}</div></div>` : ''}
|
||
${kr.length ? `<div class="info-row"><div class="lab">Ključne riječi</div><div class="val">${kr.map(k=>`<span class="chip">${esc(k)}</span>`).join('')}</div></div>` : ''}
|
||
${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 === 'search'){
|
||
$('#mBody').innerHTML = `
|
||
<div class="inline-search">
|
||
<input id="docQ" type="search" placeholder="Pretraži samo ovaj dokument...">
|
||
<button class="btn sm" onclick="searchInDoc()">Traži</button>
|
||
</div>
|
||
<div id="docSearchOut" style="color:var(--muted);font-size:0.9rem;">Upiši pojam i klikni Traži.</div>
|
||
`;
|
||
setTimeout(()=>$('#docQ').focus(), 50);
|
||
} else if(t === 'citati'){
|
||
const chunks = currentDocFull.chunks || [];
|
||
if(chunks.length === 0){
|
||
$('#mBody').innerHTML = '<div class="empty">Ovaj dokument nema RAG chunks.</div>';
|
||
return;
|
||
}
|
||
const limit = 30;
|
||
const slice = chunks.slice(0, limit);
|
||
$('#mBody').innerHTML = slice.map(c => `
|
||
<div class="chunk">
|
||
<div class="chunk-meta">Chunk #${c.chunk_index} · ${c.chunk_tokens||'?'} tokens</div>
|
||
<div>${esc((c.chunk_text||'').slice(0, 1000))}${(c.chunk_text||'').length>1000?'…':''}</div>
|
||
</div>
|
||
`).join('') + (chunks.length>limit ?
|
||
`<div class="empty">+ još ${chunks.length-limit} chunkova (skip)…</div>` : '');
|
||
}
|
||
}
|
||
|
||
async function searchInDoc(){
|
||
const q = $('#docQ').value.trim();
|
||
if(!q || q.length < 2){ return; }
|
||
$('#docSearchOut').innerHTML = '<div class="loading">Pretražujem…</div>';
|
||
try {
|
||
// Reuse global search and filter klijentski na dokument_id
|
||
const r = await api('/dokumenti/search/q?q='+encodeURIComponent(q)+'&limit=80');
|
||
const j = await r.json();
|
||
const chunks = currentDocFull.chunks || [];
|
||
// Local in-doc match: highlight chunks koji sadrže pojam
|
||
const matchingChunks = chunks.filter(c => (c.chunk_text||'').toLowerCase().includes(q.toLowerCase()));
|
||
if(matchingChunks.length === 0){
|
||
$('#docSearchOut').innerHTML = `<div class="empty">Nema pogodaka unutar ovog dokumenta za "${esc(q)}".</div>`;
|
||
return;
|
||
}
|
||
$('#docSearchOut').innerHTML = matchingChunks.slice(0, 20).map(c => `
|
||
<div class="chunk">
|
||
<div class="chunk-meta">Chunk #${c.chunk_index}</div>
|
||
<div>${highlightTerm((c.chunk_text||'').slice(0,1200), q)}${(c.chunk_text||'').length>1200?'…':''}</div>
|
||
</div>
|
||
`).join('') + (matchingChunks.length > 20 ?
|
||
`<div class="empty">+ još ${matchingChunks.length-20} pogodaka…</div>` : '');
|
||
} catch(e){
|
||
$('#docSearchOut').innerHTML = `<div class="empty">⚠️ ${esc(e.message)}</div>`;
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────
|
||
// XLSX export
|
||
// ─────────────────────────────────────────
|
||
function exportXLSX(){
|
||
if(typeof XLSX === 'undefined'){
|
||
alert('XLSX biblioteka još učitava — pričekaj sekundu i probaj opet.');
|
||
return;
|
||
}
|
||
const isSearch = activeTab === 'search';
|
||
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)
|
||
})) : displayDocs.map(d => ({
|
||
id:d.id, title:d.title, vrsta:d.vrsta, godina:d.godina,
|
||
organizacija:d.organizacija, izdano_datum:d.izdano_datum,
|
||
izvor_url:d.izvor_url, chars:d.chars
|
||
}));
|
||
if(rows.length === 0){ alert('Nema redaka za export.'); return; }
|
||
const ws = XLSX.utils.json_to_sheet(rows);
|
||
const wb = XLSX.utils.book_new();
|
||
XLSX.utils.book_append_sheet(wb, ws, 'Dokumenti');
|
||
const fname = `pgz-dokumenti-${activeTab}-${new Date().toISOString().slice(0,10)}.xlsx`;
|
||
XLSX.writeFile(wb, fname);
|
||
}
|
||
|
||
// ─────────────────────────────────────────
|
||
// Init
|
||
// ─────────────────────────────────────────
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
renderTabs();
|
||
loadAll();
|
||
$('#globalQ').addEventListener('keydown', e => { if(e.key === 'Enter') globalSearch(); });
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|