Sidebar: +ERP +CRM +Dokumenti, godišnjaci import (18 PDFs), filter helpers
- 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
This commit is contained in:
+113
-7
@@ -162,6 +162,7 @@ td.actions-col .btn { padding: 4px 8px; font-size: 11px; }
|
||||
<div class="nav-section sb-text">Sigurnost</div>
|
||||
<div class="nav-item" data-tab="audit"><span class="icon">≡</span><span class="sb-text">Audit log</span></div>
|
||||
<div class="nav-item" data-tab="security"><span class="icon">⌬</span><span class="sb-text">Sigurnost</span></div>
|
||||
<div class="nav-item" data-tab="rbac"><span class="icon">🔑</span><span class="sb-text">RBAC matrica</span></div>
|
||||
<div class="nav-section sb-text">GDPR</div>
|
||||
<div class="nav-item" data-tab="gdpr"><span class="icon">🔒</span><span class="sb-text">GDPR</span></div>
|
||||
<div class="nav-section sb-text">Drugi moduli</div>
|
||||
@@ -237,7 +238,7 @@ td.actions-col .btn { padding: 4px 8px; font-size: 11px; }
|
||||
<div class="section">
|
||||
<h3>Lista korisnika <small id="usersCount">—</small></h3>
|
||||
<table>
|
||||
<thead><tr><th>ID</th><th>E-mail</th><th>Ime</th><th>Uloga</th><th>Klub / Savez</th><th>Status</th><th>Zadnja prijava</th><th class="actions-col">Akcije</th></tr></thead>
|
||||
<thead><tr><th>ID</th><th>E-mail</th><th>Ime</th><th>Uloga</th><th>Klub / Savez</th><th>Status</th><th>2FA</th><th>Zadnja prijava</th><th class="actions-col">Akcije</th></tr></thead>
|
||||
<tbody id="usersTbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -304,6 +305,27 @@ td.actions-col .btn { padding: 4px 8px; font-size: 11px; }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="tab-rbac">
|
||||
<div class="page-header"><div><h2>RBAC matrica</h2><span class="meta">role-based access control · derivable from auth/admin_users.py</span></div></div>
|
||||
<div class="section">
|
||||
<h3>Matrica uloga & ovlasti <small>read-only</small></h3>
|
||||
<table id="rbacTable">
|
||||
<thead><tr>
|
||||
<th>Uloga</th><th>Scope</th>
|
||||
<th>List/View</th><th>Create</th><th>Edit</th><th>Reset pwd</th>
|
||||
<th>Suspend</th><th>Delete</th><th>Role change</th><th>Manage 2FA</th>
|
||||
<th>Audit log</th><th>Bulk CSV</th>
|
||||
</tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<p style="margin-top:14px;color:var(--text-2);font-size:12px">
|
||||
✓ = full · <span style="color:var(--yellow)">●</span> = u vlastitom scope-u (savez/klub) · — = nema ovlasti.<br>
|
||||
Hijerarhija: <strong>super_admin / pgz_admin</strong> > <strong>savez_admin</strong> (own savez) > <strong>klub_admin</strong> (own klub) > <strong>klub_user/trener/clan</strong> > <strong>viewer</strong>.<br>
|
||||
Role-change je rezerviran za PGŽ admine (pgz_sport.auth.admin_users:299).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="tab-gdpr">
|
||||
<div class="page-header"><h2>GDPR</h2></div>
|
||||
<div class="kpi-grid" id="gdprKpi"></div>
|
||||
@@ -363,6 +385,18 @@ td.actions-col .btn { padding: 4px 8px; font-size: 11px; }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-bg" id="userAuditModalBg">
|
||||
<div class="modal" style="width:min(820px,94vw)">
|
||||
<button class="close" onclick="closeModal('userAuditModal')">×</button>
|
||||
<h3 id="userAuditModalTitle">📜 Audit log korisnika</h3>
|
||||
<div style="font-size:12px;color:var(--text-3);margin-bottom:8px" id="userAuditMeta">—</div>
|
||||
<table>
|
||||
<thead><tr><th>Vrijeme</th><th>Akcija</th><th>Resurs</th><th>IP</th><th>Meta</th></tr></thead>
|
||||
<tbody id="userAuditTbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-bg" id="pwdModalBg">
|
||||
<div class="modal">
|
||||
<button class="close" onclick="closeModal('pwdModal')">×</button>
|
||||
@@ -466,6 +500,7 @@ function activate(tab) {
|
||||
if (tab === 'tenants') loadTenants();
|
||||
if (tab === 'audit') loadAudit();
|
||||
if (tab === 'security') loadSecurity();
|
||||
if (tab === 'rbac') loadRBAC();
|
||||
if (tab === 'gdpr') loadGdpr();
|
||||
history.replaceState(null, '', '#' + tab);
|
||||
}
|
||||
@@ -552,14 +587,57 @@ async function loadUsers() {
|
||||
<td>${roleBadge(u.user_type)}</td>
|
||||
<td>${escapeHtml(u.klub_naziv || u.savez_naziv || (u.klub_id?'klub#'+u.klub_id:u.savez_id?'savez#'+u.savez_id:'—'))}</td>
|
||||
<td>${statusBadge(u.aktivan)}${u.locked_until?'<br><span class="badge red">Locked</span>':''}</td>
|
||||
<td><span class="badge gray" id="tfa-${u.id}" data-2fa="?">…</span></td>
|
||||
<td>${fmtDateTime(u.last_login)}</td>
|
||||
<td class="actions-col">
|
||||
<button class="btn" onclick="editUser(${u.id})">✎</button>
|
||||
<button class="btn" onclick="resetPwd(${u.id})">🔑</button>
|
||||
<button class="btn" onclick="toggleSuspend(${u.id}, ${u.aktivan})">${u.aktivan?'⏸':'▶'}</button>
|
||||
<button class="btn danger" onclick="deleteUser(${u.id}, '${escapeHtml(u.email)}')">✕</button>
|
||||
<button class="btn" title="Uredi" onclick="editUser(${u.id})">✎</button>
|
||||
<button class="btn" title="Reset lozinke" onclick="resetPwd(${u.id})">🔑</button>
|
||||
<button class="btn" title="2FA" onclick="toggle2FA(${u.id})">🛡</button>
|
||||
<button class="btn" title="Audit log" onclick="openUserAudit(${u.id}, '${escapeHtml(u.email)}')">📜</button>
|
||||
<button class="btn" title="${u.aktivan?'Suspendiraj':'Aktiviraj'}" onclick="toggleSuspend(${u.id}, ${u.aktivan})">${u.aktivan?'⏸':'▶'}</button>
|
||||
<button class="btn danger" title="Obriši" onclick="deleteUser(${u.id}, '${escapeHtml(u.email)}')">✕</button>
|
||||
</td></tr>
|
||||
`).join('') || '<tr><td colspan="8" class="empty">Nema korisnika</td></tr>';
|
||||
`).join('') || '<tr><td colspan="9" class="empty">Nema korisnika</td></tr>';
|
||||
// Lazy-load per-user 2FA badges (parallel)
|
||||
(data.results || []).forEach(u => loadUser2FA(u.id));
|
||||
}
|
||||
|
||||
async function loadUser2FA(uid) {
|
||||
const cell = document.getElementById('tfa-' + uid);
|
||||
if (!cell) return;
|
||||
const r = await apiJson('/admin/users/' + uid + '/2fa-status');
|
||||
if (!r) { cell.textContent = '—'; cell.className = 'badge gray'; return; }
|
||||
const en = !!r.enabled;
|
||||
cell.textContent = en ? '✓ ON' : 'OFF';
|
||||
cell.className = 'badge ' + (en ? 'green' : 'gray');
|
||||
cell.dataset['2fa'] = en ? '1' : '0';
|
||||
}
|
||||
|
||||
async function toggle2FA(uid) {
|
||||
const cell = document.getElementById('tfa-' + uid);
|
||||
const en = cell && cell.dataset['2fa'] === '1';
|
||||
if (!en) {
|
||||
return alert('2FA je trenutno OFF za ovog korisnika.\n\n2FA se aktivira samo od strane korisnika (osobni QR scan).\nAdmin može samo prisilno deaktivirati ako korisnik izgubi autentifikator.');
|
||||
}
|
||||
if (!confirm('Prisilno isključiti 2FA za ovog korisnika?\nKorisnik gubi sve recovery kodove i mora ponovo postaviti 2FA.\nSesije se poništavaju.')) return;
|
||||
const r = await apiJson('/admin/users/' + uid + '/2fa-disable', {method:'POST'});
|
||||
if (r?.status === 'ok') { toast('2FA isključen'); loadUser2FA(uid); }
|
||||
else toast(r?.detail || 'Greška', 'error');
|
||||
}
|
||||
|
||||
async function openUserAudit(uid, email) {
|
||||
$('#userAuditModalTitle').textContent = '📜 Audit log — #' + uid;
|
||||
$('#userAuditMeta').textContent = email + ' · zadnjih 100 događaja';
|
||||
$('#userAuditTbody').innerHTML = '<tr><td colspan="5" class="empty">Učitavam…</td></tr>';
|
||||
openModal('userAuditModal');
|
||||
const d = await apiJson('/admin/audit?user_id=' + uid + '&limit=100');
|
||||
$('#userAuditTbody').innerHTML = (d?.results || []).map(a => `
|
||||
<tr><td class="audit-row">${fmtDateTime(a.created_at)}</td>
|
||||
<td><span class="audit-action">${escapeHtml(a.action||'')}</span></td>
|
||||
<td>${escapeHtml(a.resource_type||'—')} ${a.resource_id??''}</td>
|
||||
<td class="audit-row">${escapeHtml(a.ip_address||'—')}</td>
|
||||
<td class="audit-row" style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title='${escapeHtml(JSON.stringify(a.meta||{}))}'>${escapeHtml(JSON.stringify(a.meta||{}).substring(0,80))}</td></tr>
|
||||
`).join('') || '<tr><td colspan="5" class="empty">Nema događaja</td></tr>';
|
||||
}
|
||||
['usrQ','usrTenant','usrRole','usrStatus','usrLimit'].forEach(id => {
|
||||
$('#'+id).addEventListener('input', () => { clearTimeout(usersDebounce); usersDebounce = setTimeout(loadUsers, 300); });
|
||||
@@ -754,6 +832,34 @@ $('#btnDisable2FA').addEventListener('click', async () => {
|
||||
else toast(r?.detail || 'Greška', 'error');
|
||||
});
|
||||
|
||||
// RBAC matrix (read-only, derived from auth/admin_users.py _can_manage)
|
||||
const RBAC_ROWS = [
|
||||
// [role, scope, list, create, edit, reset, suspend, delete, role_change, manage_2fa, audit, bulk_csv]
|
||||
['super_admin', 'global', '✓','✓','✓','✓','✓','✓','✓','✓','✓','✓'],
|
||||
['pgz_admin', 'global', '✓','✓','✓','✓','✓','✓','✓','✓','✓','✓'],
|
||||
['pgz_finance', 'global', '✓','—','—','—','—','—','—','—','✓','—'],
|
||||
['savez_admin', 'own savez', '●','●','●','●','●','●','—','●','●','—'],
|
||||
['savez_user', 'own savez', '●','—','—','—','—','—','—','—','—','—'],
|
||||
['klub_admin', 'own klub', '●','●','●','●','●','●','—','●','●','—'],
|
||||
['klub_trener', 'own klub (RO)', '●','—','—','—','—','—','—','—','—','—'],
|
||||
['klub_user', 'own klub (RO)', '●','—','—','—','—','—','—','—','—','—'],
|
||||
['klub_clan', 'self only', '●','—','—','—','—','—','—','—','—','—'],
|
||||
['viewer', 'read-only', '●','—','—','—','—','—','—','—','—','—'],
|
||||
];
|
||||
function rbacCell(v) {
|
||||
if (v === '✓') return '<span style="color:var(--green);font-weight:600">✓</span>';
|
||||
if (v === '●') return '<span style="color:var(--yellow);font-weight:600">●</span>';
|
||||
return '<span style="color:var(--text-3)">—</span>';
|
||||
}
|
||||
function loadRBAC() {
|
||||
const tb = $('#rbacTable tbody');
|
||||
tb.innerHTML = RBAC_ROWS.map(r => `
|
||||
<tr><td>${roleBadge(r[0])}</td><td><small style="color:var(--text-3)">${escapeHtml(r[1])}</small></td>
|
||||
${r.slice(2).map(rbacCell).map(c=>'<td style="text-align:center">'+c+'</td>').join('')}
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// GDPR
|
||||
async function loadGdpr() {
|
||||
const er = await apiJson('/admin/gdpr/erasure-requests');
|
||||
@@ -818,7 +924,7 @@ $('#cookieNecessary').addEventListener('click', () => saveConsent(true, false, f
|
||||
$('#userAvatar').textContent = (me.full_name || me.email || '?')[0].toUpperCase();
|
||||
await loadTenantSelect();
|
||||
const initialTab = (location.hash || '#users').replace('#','');
|
||||
activate(['overview','users','tenants','audit','security','gdpr'].includes(initialTab) ? initialTab : 'users');
|
||||
activate(['overview','users','tenants','audit','security','rbac','gdpr'].includes(initialTab) ? initialTab : 'users');
|
||||
showCookieIfNeeded();
|
||||
})();
|
||||
</script>
|
||||
|
||||
+40
-3
@@ -418,6 +418,25 @@ async function api(path){
|
||||
catch(e){ return null; }
|
||||
}
|
||||
|
||||
// ─── Sport-aware enrichment source (cached) ───
|
||||
const _appEnrichSrc = {};
|
||||
async function appOpenEnrichSource(sport, naziv){
|
||||
if(!sport){ alert('Sport nije naveden — ne mogu odrediti savez.'); return; }
|
||||
const k = (sport||'').toLowerCase();
|
||||
let src = _appEnrichSrc[k];
|
||||
if(!src){
|
||||
const d = await api('/v2/enrich-sources?sport='+encodeURIComponent(sport));
|
||||
src = d && d.match;
|
||||
if(src) _appEnrichSrc[k] = src;
|
||||
}
|
||||
if(!src){ alert('Nema definiranog izvora za sport: '+sport); return; }
|
||||
const base = (src.base_url||'').replace(/\/$/,'');
|
||||
const url = ((src.sport||'').toLowerCase()==='nogomet')
|
||||
? base + '/klubovi?q=' + encodeURIComponent(naziv||'')
|
||||
: base + '/?s=' + encodeURIComponent(naziv||'');
|
||||
window.open(url, '_blank', 'noopener');
|
||||
}
|
||||
|
||||
// JWT-aware fetch wrapper
|
||||
function getToken(){
|
||||
try {
|
||||
@@ -485,13 +504,15 @@ const NAV_BY_ROLE = {
|
||||
pgz: [
|
||||
{id:'profil', ic:'\u{1F464}', label:'Moj profil'},
|
||||
{id:'dashboard', ic:'\u{1F4CA}', label:'Dashboard'},
|
||||
{id:'korisnici', ic:'\u{1F465}', label:'Korisnici'},
|
||||
{id:'korisnici', ic:'\u{1F465}', label:'Korisnici', href:'/admin/users'},
|
||||
{id:'savezi', ic:'\u{1F3C5}', label:'Savezi'},
|
||||
{id:'klubovi', ic:'⬢', label:'Klubovi'},
|
||||
{id:'sportasi', ic:'\u{1F464}', label:'Sportaši'},
|
||||
{id:'financije', ic:'€', label:'Financije'},
|
||||
{id:'erp', ic:'\u{1F4BC}', label:'ERP', href:'/erp/full'},
|
||||
{id:'crm', ic:'\u{1F4DD}', label:'CRM', href:'/crm/v2'},
|
||||
{id:'dokumenti', ic:'\u{1F4D6}', label:'Dokumenti'},
|
||||
{id:'racuni', ic:'\u{1F9FE}', label:'Računi (OCR)'},
|
||||
{id:'crm', ic:'\u{1F4DD}', label:'CRM'},
|
||||
{id:'kalendar', ic:'\u{1F4C5}', label:'Kalendar'},
|
||||
{id:'audit', ic:'\u{1F50D}', label:'Audit log'},
|
||||
{id:'forenzika', ic:'⚠', label:'Forenzika', badge:11},
|
||||
@@ -626,7 +647,7 @@ async function showDetail(kind, id, title){
|
||||
if(!d){ body = '<div class="empty">Klub nije pronađen.</div>'; }
|
||||
else body = `
|
||||
<h2 style="font-size:18px;color:var(--t0);margin-bottom:6px">${esc(d.naziv||'—')}</h2>
|
||||
<div style="font-size:11px;color:var(--t2);margin-bottom:14px">${esc(d.savez||'')} · ${esc(d.grad||'')}</div>
|
||||
<div style="font-size:11px;color:var(--t2);margin-bottom:14px">${esc(d.savez||'')} · ${esc(d.sport||'')} · ${esc(d.grad||'')}</div>
|
||||
<div class="kv">
|
||||
<div class="k">OIB</div><div class="v">${d.oib?esc(formatOib(d.oib,{klub_id:d.id,savez_id:d.savez_id})):'—'}</div>
|
||||
<div class="k">Predsjednik</div><div class="v">${esc(d.predsjednik||'—')}</div>
|
||||
@@ -634,6 +655,9 @@ async function showDetail(kind, id, title){
|
||||
<div class="k">Email</div><div class="v">${esc(d.email||'—')}</div>
|
||||
<div class="k">Telefon</div><div class="v">${esc(d.telefon||'—')}</div>
|
||||
<div class="k">Članova</div><div class="v">${fmt(d.broj_clanova||'—')}</div>
|
||||
</div>
|
||||
<div style="margin-top:14px;display:flex;gap:8px;flex-wrap:wrap">
|
||||
<button class="btn" onclick="appOpenEnrichSource(${JSON.stringify(d.sport||'')}, ${JSON.stringify(d.naziv||'')})">🌐 Obogati podatke (sport-savez)</button>
|
||||
</div>`;
|
||||
} else if(kind === 'zahtjev'){
|
||||
const z = MOCK.zahtjevi_pending.concat(MOCK.savez_zahtjevi||[]).find(x => x.id===id || x.naziv===id) || {};
|
||||
@@ -2064,6 +2088,19 @@ function toggleMobileSidebar(){
|
||||
}
|
||||
backdrop.classList.toggle('show');
|
||||
}
|
||||
|
||||
// hrefnav handler — for nav items that have href (external page)
|
||||
function navItemClick(item){
|
||||
if(item && item.href){ window.location.href = item.href; return; }
|
||||
if(item && item.id) navTo(item.id);
|
||||
}
|
||||
|
||||
// PGŽ priority filter helpers (CRISIS V4)
|
||||
window._pgz_filter_priority = window._pgz_filter_priority || false;
|
||||
window.togglePGZFilter = function(){
|
||||
window._pgz_filter_priority = !window._pgz_filter_priority;
|
||||
if(typeof loadSection === 'function') loadSection();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+1108
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,751 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,943 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
erp_full.html — FULL ERP (SAP-Lite) za PGŽ Sport
|
||||
Author: Damir Radulić <dradulic@outlook.com> / damir@rinet.one
|
||||
Version: 1.0.0
|
||||
Date: 2026-05-05
|
||||
Description: Sub-tabs Dnevnik | Glavna knjiga | Partneri | Računi | PDV | Plaće | Proračun | Izvještaji
|
||||
-->
|
||||
<html lang="hr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>PGŽ SPORT — ERP (SAP-Lite)</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{
|
||||
--pgz-blue:#003087; --pgz-blue2:#004CC4; --pgz-gold:#F4C430;
|
||||
--bg0:#08090e; --bg1:#0d1021; --bg2:#111628; --bg3:#161d35; --bg4:#1c2542;
|
||||
--rim:#1e2a50; --rim2:#283560;
|
||||
--t0:#fff; --t1:#e2e6f0; --t2:#8a95b4; --t4:#4e5a7a;
|
||||
--green:#00e88f; --red:#ff2d55; --amber:#f59e0b; --cyan:#00c8e8;
|
||||
--font:'Inter',sans-serif; --mono:'JetBrains Mono',monospace;
|
||||
}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
html,body{height:100%}
|
||||
body{font-family:var(--font);background:var(--bg0);color:var(--t1);font-size:13px;overflow-x:hidden}
|
||||
button,input,select,textarea{font-family:inherit;font-size:inherit;outline:none}
|
||||
::-webkit-scrollbar{width:8px;height:8px}
|
||||
::-webkit-scrollbar-track{background:var(--bg1)}
|
||||
::-webkit-scrollbar-thumb{background:var(--rim2);border-radius:4px}
|
||||
|
||||
.main{padding:0 0 0 0;flex:1;min-width:0}
|
||||
.tb{background:var(--bg1);border-bottom:1px solid var(--rim);padding:12px 22px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:5}
|
||||
.tb-t{font-size:15px;font-weight:700;color:var(--t0)}
|
||||
.tb-s{font-size:11px;color:var(--t2)}
|
||||
.content{padding:18px 22px}
|
||||
|
||||
.tabs{display:flex;gap:4px;border-bottom:1px solid var(--rim);margin-bottom:14px;flex-wrap:wrap}
|
||||
.tab{padding:9px 16px;cursor:pointer;color:var(--t2);font-weight:600;font-size:12px;border-bottom:2px solid transparent;transition:all .15s;background:none;border-left:0;border-right:0;border-top:0}
|
||||
.tab:hover{color:var(--t1)}
|
||||
.tab.active{color:var(--pgz-gold);border-bottom-color:var(--pgz-gold)}
|
||||
|
||||
.panel{display:none}
|
||||
.panel.active{display:block}
|
||||
|
||||
.card{background:var(--bg2);border:1px solid var(--rim);border-radius:8px;padding:14px;margin-bottom:14px}
|
||||
.card-h{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--rim)}
|
||||
.card-t{font-weight:700;color:var(--t0);font-size:13px}
|
||||
|
||||
.toolbar{display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap}
|
||||
.toolbar input,.toolbar select{background:var(--bg2);border:1px solid var(--rim);border-radius:5px;padding:7px 10px;color:var(--t1);font-size:12px}
|
||||
.toolbar input:focus,.toolbar select:focus{border-color:var(--pgz-blue2)}
|
||||
.toolbar label{font-size:11px;color:var(--t2);display:flex;align-items:center;gap:6px}
|
||||
.btn{background:var(--pgz-blue2);border:0;color:#fff;padding:7px 14px;border-radius:5px;cursor:pointer;font-size:12px;font-weight:600;transition:background .15s}
|
||||
.btn:hover{background:var(--pgz-blue)}
|
||||
.btn.gold{background:var(--pgz-gold);color:var(--bg0)}
|
||||
.btn.gold:hover{background:#e0b220}
|
||||
.btn.green{background:var(--green);color:var(--bg0)}
|
||||
.btn.red{background:var(--red);color:#fff}
|
||||
.btn.sec{background:var(--bg3);color:var(--t1);border:1px solid var(--rim)}
|
||||
|
||||
table{width:100%;border-collapse:collapse;font-size:12px}
|
||||
table th{background:var(--bg3);color:var(--t2);text-transform:uppercase;font-size:10px;letter-spacing:.5px;padding:8px 10px;text-align:left;border-bottom:1px solid var(--rim);font-weight:700}
|
||||
table td{padding:7px 10px;border-bottom:1px solid var(--rim);color:var(--t1)}
|
||||
table tbody tr{cursor:pointer;transition:background .15s}
|
||||
table tbody tr:hover{background:var(--bg3)}
|
||||
.num{font-family:var(--mono);text-align:right}
|
||||
.tbl-wrap{overflow-x:auto;max-height:600px;overflow-y:auto;border:1px solid var(--rim);border-radius:6px}
|
||||
|
||||
.modal-bg{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:100;align-items:flex-start;justify-content:center;padding-top:40px}
|
||||
.modal-bg.show{display:flex}
|
||||
.modal{background:var(--bg1);border:1px solid var(--rim);border-radius:8px;padding:18px;width:min(720px,94vw);max-height:90vh;overflow-y:auto}
|
||||
.modal h3{font-size:14px;font-weight:700;color:var(--pgz-gold);margin-bottom:14px;padding-bottom:8px;border-bottom:1px solid var(--rim)}
|
||||
.form-row{display:grid;grid-template-columns:140px 1fr;gap:8px;margin-bottom:8px;align-items:center}
|
||||
.form-row label{font-size:11px;color:var(--t2)}
|
||||
.form-row input,.form-row select,.form-row textarea{width:100%;background:var(--bg2);border:1px solid var(--rim);border-radius:4px;padding:6px 9px;color:var(--t1);font-size:12px}
|
||||
.form-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:14px;padding-top:10px;border-top:1px solid var(--rim)}
|
||||
|
||||
.kpi-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin-bottom:14px}
|
||||
.kpi{background:linear-gradient(135deg,var(--bg2) 0%,var(--bg1) 100%);border:1px solid var(--rim);border-radius:8px;padding:12px 14px;position:relative}
|
||||
.kpi::before{content:"";position:absolute;top:0;left:0;width:3px;height:100%;background:var(--pgz-gold)}
|
||||
.kpi.g::before{background:var(--green)}
|
||||
.kpi.r::before{background:var(--red)}
|
||||
.kpi-l{font-size:10.5px;color:var(--t2);text-transform:uppercase;letter-spacing:1px;font-weight:600}
|
||||
.kpi-v{font-size:22px;font-weight:800;color:var(--t0);margin-top:4px;font-family:var(--mono)}
|
||||
|
||||
.badge{display:inline-block;padding:2px 7px;border-radius:3px;font-size:10px;font-weight:600;text-transform:uppercase}
|
||||
.badge.nacrt{background:var(--bg4);color:var(--t1)}
|
||||
.badge.knjizen{background:var(--green);color:var(--bg0)}
|
||||
.badge.placen{background:var(--pgz-gold);color:var(--bg0)}
|
||||
.badge.otkazan{background:var(--red);color:#fff}
|
||||
|
||||
.dnev-line-row{display:grid;grid-template-columns:140px 1fr 100px 100px 1fr 30px;gap:6px;margin-bottom:6px;align-items:center}
|
||||
.dnev-line-row input,.dnev-line-row select{background:var(--bg2);border:1px solid var(--rim);border-radius:4px;padding:5px 8px;color:var(--t1);font-size:12px;width:100%}
|
||||
.dnev-balans{padding:8px;background:var(--bg3);border-radius:4px;margin-top:8px;font-family:var(--mono);font-size:11px}
|
||||
.dnev-balans.ok{color:var(--green)}
|
||||
.dnev-balans.bad{color:var(--red)}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/shared/sidebar.css">
|
||||
<script src="/static/shared/sidebar.js" defer data-active="erp_full"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<main class="main">
|
||||
<div class="tb">
|
||||
<div>
|
||||
<div class="tb-t">ERP — SAP-Lite</div>
|
||||
<div class="tb-s">Dvostavno knjigovodstvo · HR-RRIF kontni plan · FINA e-Račun</div>
|
||||
</div>
|
||||
<div class="tb-s"><span style="color:var(--green)">●</span> /api/v2/erp/*</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="tabs">
|
||||
<button class="tab active" data-panel="dnevnik">📓 Dnevnik</button>
|
||||
<button class="tab" data-panel="glavna">📊 Glavna knjiga</button>
|
||||
<button class="tab" data-panel="partneri">🤝 Partneri</button>
|
||||
<button class="tab" data-panel="racuni">🧾 Računi</button>
|
||||
<button class="tab" data-panel="pdv">% PDV</button>
|
||||
<button class="tab" data-panel="place">💼 Plaće</button>
|
||||
<button class="tab" data-panel="proracun">€ Proračun</button>
|
||||
<button class="tab" data-panel="izvjestaji">📈 Izvještaji</button>
|
||||
<button class="tab" data-panel="kontni">📚 Kontni plan</button>
|
||||
</div>
|
||||
|
||||
<!-- ============ DNEVNIK ============ -->
|
||||
<section class="panel active" id="panel-dnevnik">
|
||||
<div class="card">
|
||||
<div class="card-h">
|
||||
<div class="card-t">Dnevnik knjiženja</div>
|
||||
<button class="btn gold" onclick="openDnevnikModal()">+ Novi zapis</button>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<label>Godina <input type="number" id="dnev-godina" value="2026" style="width:90px"></label>
|
||||
<label>Tip <select id="dnev-tip"><option value="">— svi —</option><option value="rucno">Ručno</option><option value="racun_u">Račun ulazni</option><option value="racun_i">Račun izlazni</option><option value="placa">Plaća</option><option value="storno">Storno</option></select></label>
|
||||
<button class="btn" onclick="loadDnevnik()">Osvježi</button>
|
||||
</div>
|
||||
<div class="tbl-wrap">
|
||||
<table id="dnev-tbl"><thead><tr><th>#</th><th>Datum</th><th>Opis</th><th>Tip</th><th class="num">Stavki</th><th class="num">Ukupno</th><th>Akcije</th></tr></thead><tbody></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ GLAVNA KNJIGA ============ -->
|
||||
<section class="panel" id="panel-glavna">
|
||||
<div class="card">
|
||||
<div class="card-h">
|
||||
<div class="card-t">Glavna knjiga (saldo po kontu)</div>
|
||||
<button class="btn sec" onclick="exportXlsx('glavna-knjiga', 2026)">⬇ XLSX</button>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<label>Klasa <select id="gk-klasa"><option value="">— sve —</option><option>0</option><option>1</option><option>2</option><option>3</option><option>4</option><option>7</option><option>9</option></select></label>
|
||||
<button class="btn" onclick="loadGlavnaKnjiga()">Osvježi</button>
|
||||
</div>
|
||||
<div class="tbl-wrap">
|
||||
<table id="gk-tbl"><thead><tr><th>Šifra</th><th>Naziv</th><th>Klasa</th><th>Vrsta</th><th class="num">Duguje</th><th class="num">Potražuje</th><th class="num">Saldo</th><th class="num">Stavki</th></tr></thead><tbody></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ PARTNERI ============ -->
|
||||
<section class="panel" id="panel-partneri">
|
||||
<div class="card">
|
||||
<div class="card-h">
|
||||
<div class="card-t">Partneri (kupci/dobavljači)</div>
|
||||
<button class="btn gold" onclick="openPartnerModal()">+ Novi partner</button>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<input id="part-q" placeholder="Pretraži (naziv/OIB)…">
|
||||
<label>Vrsta <select id="part-vrsta"><option value="">— sve —</option><option value="kupac">Kupac</option><option value="dobavljac">Dobavljač</option><option value="oba">Oba</option></select></label>
|
||||
<button class="btn" onclick="loadPartneri()">Osvježi</button>
|
||||
</div>
|
||||
<div class="tbl-wrap">
|
||||
<table id="part-tbl"><thead><tr><th>#</th><th>OIB</th><th>Naziv</th><th>Vrsta</th><th>Grad</th><th>IBAN</th><th class="num">Saldo</th><th>Akcije</th></tr></thead><tbody></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ RAČUNI ============ -->
|
||||
<section class="panel" id="panel-racuni">
|
||||
<div class="card">
|
||||
<div class="card-h">
|
||||
<div class="card-t">Računi</div>
|
||||
<div style="display:flex;gap:6px">
|
||||
<button class="btn gold" onclick="openRacunModal('ulazni')">+ Ulazni</button>
|
||||
<button class="btn green" onclick="openRacunModal('izlazni')">+ Izlazni</button>
|
||||
<label class="btn sec" style="cursor:pointer;display:inline-flex;align-items:center;gap:6px">
|
||||
📥 Import e-Račun XML
|
||||
<input type="file" id="eracun-file" accept=".xml,application/xml" style="display:none" onchange="importERacun()">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<label>Tip <select id="rac-tip"><option value="ulazni">Ulazni</option><option value="izlazni">Izlazni</option></select></label>
|
||||
<label>Status <select id="rac-status"><option value="">— svi —</option><option value="nacrt">Nacrt</option><option value="knjizen">Knjižen</option><option value="placen">Plaćen</option><option value="otkazan">Otkazan</option></select></label>
|
||||
<label>Godina <input type="number" id="rac-godina" value="2026" style="width:90px"></label>
|
||||
<button class="btn" onclick="loadRacuni()">Osvježi</button>
|
||||
</div>
|
||||
<div class="tbl-wrap">
|
||||
<table id="rac-tbl"><thead><tr><th>#</th><th>Broj</th><th>Datum</th><th>Partner</th><th>OIB</th><th class="num">Neto</th><th class="num">PDV</th><th class="num">Brutto</th><th>Status</th><th>Akcije</th></tr></thead><tbody></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ PDV ============ -->
|
||||
<section class="panel" id="panel-pdv">
|
||||
<div class="card">
|
||||
<div class="card-h">
|
||||
<div class="card-t">PDV — Knjige + Obrazac</div>
|
||||
<div style="display:flex;gap:6px">
|
||||
<button class="btn sec" onclick="exportXlsx('pdv-u', document.getElementById('pdv-godina').value, document.getElementById('pdv-mjesec').value)">⬇ U XLSX</button>
|
||||
<button class="btn sec" onclick="exportXlsx('pdv-i', document.getElementById('pdv-godina').value, document.getElementById('pdv-mjesec').value)">⬇ I XLSX</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<label>Godina <input type="number" id="pdv-godina" value="2026" style="width:90px"></label>
|
||||
<label>Mjesec <input type="number" id="pdv-mjesec" min="1" max="12" placeholder="1-12" style="width:80px"></label>
|
||||
<button class="btn" onclick="loadPdv()">Osvježi</button>
|
||||
</div>
|
||||
<div id="pdv-summary" class="kpi-grid"></div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
|
||||
<div>
|
||||
<h4 style="font-size:12px;color:var(--t2);margin-bottom:8px">KNJIGA U-RA (ulazni)</h4>
|
||||
<div class="tbl-wrap"><table id="pdv-u-tbl"><thead><tr><th>Broj</th><th>Datum</th><th>Partner</th><th>OIB</th><th class="num">Neto</th><th class="num">PDV</th></tr></thead><tbody></tbody></table></div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 style="font-size:12px;color:var(--t2);margin-bottom:8px">KNJIGA I-RA (izlazni)</h4>
|
||||
<div class="tbl-wrap"><table id="pdv-i-tbl"><thead><tr><th>Broj</th><th>Datum</th><th>Partner</th><th>OIB</th><th class="num">Neto</th><th class="num">PDV</th></tr></thead><tbody></tbody></table></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ PLAĆE ============ -->
|
||||
<section class="panel" id="panel-place">
|
||||
<div class="card">
|
||||
<div class="card-h">
|
||||
<div class="card-t">Zaposlenici i obračun plaća</div>
|
||||
<div style="display:flex;gap:6px">
|
||||
<button class="btn gold" onclick="openZapModal()">+ Zaposlenik</button>
|
||||
<button class="btn green" onclick="openPlacaModal()">€ Obračun plaće</button>
|
||||
</div>
|
||||
</div>
|
||||
<h4 style="font-size:12px;color:var(--t2);margin:8px 0">Zaposlenici</h4>
|
||||
<div class="tbl-wrap" style="margin-bottom:14px">
|
||||
<table id="zap-tbl"><thead><tr><th>#</th><th>OIB</th><th>Ime</th><th>Prezime</th><th>Klub</th><th>Mjesto</th><th class="num">Bruto</th><th>Aktivan</th></tr></thead><tbody></tbody></table>
|
||||
</div>
|
||||
<h4 style="font-size:12px;color:var(--t2);margin:8px 0">Obračuni plaća</h4>
|
||||
<div class="toolbar">
|
||||
<label>Godina <input type="number" id="pl-godina" value="2026" style="width:90px"></label>
|
||||
<label>Mjesec <input type="number" id="pl-mjesec" placeholder="1-12" style="width:80px"></label>
|
||||
<button class="btn" onclick="loadPlace()">Osvježi</button>
|
||||
</div>
|
||||
<div class="tbl-wrap">
|
||||
<table id="pl-tbl"><thead><tr><th>#</th><th>Zaposlenik</th><th>God/Mj</th><th class="num">Bruto</th><th class="num">Doprinosi iz</th><th class="num">Dohodnina</th><th class="num">Neto</th><th class="num">Doprinosi na</th><th class="num">Trošak</th></tr></thead><tbody></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ PRORAČUN ============ -->
|
||||
<section class="panel" id="panel-proracun">
|
||||
<div class="card">
|
||||
<div class="card-h"><div class="card-t">Proračun PGŽ Sport (po godinama)</div></div>
|
||||
<div class="tbl-wrap">
|
||||
<table id="pr-tbl"><thead><tr><th>Godina</th><th class="num">Proračun PGŽ</th><th class="num">Rebalans 1</th><th class="num">Rebalans 2</th><th class="num">Ukupno PGŽ</th><th class="num">Ministarstvo</th><th class="num">Ukupno</th><th>Napomena</th></tr></thead><tbody></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ IZVJEŠTAJI ============ -->
|
||||
<section class="panel" id="panel-izvjestaji">
|
||||
<div class="card">
|
||||
<div class="card-h">
|
||||
<div class="card-t">Izvještaji (Bilanca · PnL · Cashflow)</div>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<label>Tip <select id="iz-tip"><option value="bilanca">Bilanca</option><option value="pnl">PnL (Račun dobiti/gubitka)</option><option value="cashflow">Cashflow</option></select></label>
|
||||
<label>Godina <input type="number" id="iz-godina" value="2026" style="width:90px"></label>
|
||||
<button class="btn" onclick="loadIzvjestaj()">Generiraj</button>
|
||||
<button class="btn sec" onclick="exportXlsx(document.getElementById('iz-tip').value, document.getElementById('iz-godina').value)">⬇ XLSX</button>
|
||||
<button class="btn sec" onclick="exportPdf(document.getElementById('iz-tip').value, document.getElementById('iz-godina').value)">⬇ PDF</button>
|
||||
</div>
|
||||
<div id="iz-out"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ KONTNI PLAN ============ -->
|
||||
<section class="panel" id="panel-kontni">
|
||||
<div class="card">
|
||||
<div class="card-h">
|
||||
<div class="card-t">Kontni plan (HR-RRIF)</div>
|
||||
<button class="btn gold" onclick="openKontoModal()">+ Novi konto</button>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<input id="kp-q" placeholder="Šifra ili naziv…">
|
||||
<label>Klasa <select id="kp-klasa"><option value="">— sve —</option><option>0</option><option>1</option><option>2</option><option>3</option><option>4</option><option>7</option><option>9</option></select></label>
|
||||
<label>Vrsta <select id="kp-vrsta"><option value="">— sve —</option><option value="aktiva">Aktiva</option><option value="pasiva">Pasiva</option><option value="prihod">Prihod</option><option value="rashod">Rashod</option><option value="kapital">Kapital</option></select></label>
|
||||
<button class="btn" onclick="loadKontniPlan()">Osvježi</button>
|
||||
</div>
|
||||
<div class="tbl-wrap">
|
||||
<table id="kp-tbl"><thead><tr><th>Šifra</th><th>Naziv</th><th>Klasa</th><th>Vrsta</th><th>Aktivan</th><th>Akcije</th></tr></thead><tbody></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- ===== MODALS ===== -->
|
||||
<div class="modal-bg" id="m-konto" onclick="if(event.target===this)closeModal('m-konto')">
|
||||
<div class="modal">
|
||||
<h3>Novi / izmjena konta</h3>
|
||||
<div class="form-row"><label>Šifra</label><input id="k-sifra"></div>
|
||||
<div class="form-row"><label>Naziv</label><input id="k-naziv"></div>
|
||||
<div class="form-row"><label>Klasa</label><input type="number" id="k-klasa" min="0" max="9"></div>
|
||||
<div class="form-row"><label>Vrsta</label><select id="k-vrsta"><option value="aktiva">Aktiva</option><option value="pasiva">Pasiva</option><option value="prihod">Prihod</option><option value="rashod">Rashod</option><option value="kapital">Kapital</option><option value="izvanbilanca">Izvanbilanca</option></select></div>
|
||||
<div class="form-actions">
|
||||
<button class="btn sec" onclick="closeModal('m-konto')">Odustani</button>
|
||||
<button class="btn gold" onclick="saveKonto()">Spremi</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-bg" id="m-partner" onclick="if(event.target===this)closeModal('m-partner')">
|
||||
<div class="modal">
|
||||
<h3>Novi / izmjena partnera</h3>
|
||||
<div class="form-row"><label>Naziv</label><input id="p-naziv"></div>
|
||||
<div class="form-row"><label>OIB</label><input id="p-oib" maxlength="11"></div>
|
||||
<div class="form-row"><label>Vrsta</label><select id="p-vrsta"><option value="oba">Oba</option><option value="kupac">Kupac</option><option value="dobavljac">Dobavljač</option></select></div>
|
||||
<div class="form-row"><label>IBAN</label><input id="p-iban"></div>
|
||||
<div class="form-row"><label>Adresa</label><input id="p-adresa"></div>
|
||||
<div class="form-row"><label>Grad</label><input id="p-grad"></div>
|
||||
<div class="form-row"><label>Email</label><input id="p-email"></div>
|
||||
<div class="form-row"><label>Telefon</label><input id="p-telefon"></div>
|
||||
<div class="form-actions">
|
||||
<button class="btn sec" onclick="closeModal('m-partner')">Odustani</button>
|
||||
<button class="btn gold" onclick="savePartner()">Spremi</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-bg" id="m-dnev" onclick="if(event.target===this)closeModal('m-dnev')">
|
||||
<div class="modal" style="width:min(960px,96vw)">
|
||||
<h3>Novi zapis u dnevniku</h3>
|
||||
<div class="form-row"><label>Datum</label><input type="date" id="d-datum"></div>
|
||||
<div class="form-row"><label>Opis</label><input id="d-opis"></div>
|
||||
<div class="form-row"><label>Tip dokumenta</label><select id="d-tip"><option value="rucno">Ručno</option><option value="racun_u">Račun ulazni</option><option value="racun_i">Račun izlazni</option><option value="placa">Plaća</option></select></div>
|
||||
<h4 style="font-size:11px;color:var(--t2);margin:14px 0 6px">STAVKE (D=duguje, P=potražuje, samo jedno > 0)</h4>
|
||||
<div id="d-lines"></div>
|
||||
<button class="btn sec" onclick="addDnevLine()" style="margin-top:8px">+ Dodaj stavku</button>
|
||||
<div id="d-balans" class="dnev-balans">Duguje: 0,00 · Potražuje: 0,00</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn sec" onclick="closeModal('m-dnev')">Odustani</button>
|
||||
<button class="btn gold" onclick="saveDnev()">Spremi zapis</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-bg" id="m-rac" onclick="if(event.target===this)closeModal('m-rac')">
|
||||
<div class="modal" style="width:min(900px,96vw)">
|
||||
<h3 id="m-rac-title">Novi račun</h3>
|
||||
<div class="form-row"><label>Broj</label><input id="r-broj"></div>
|
||||
<div class="form-row"><label>Partner</label><select id="r-partner"></select></div>
|
||||
<div class="form-row"><label>Datum izdavanja</label><input type="date" id="r-datum"></div>
|
||||
<div class="form-row"><label>Dospijeće</label><input type="date" id="r-dospjece"></div>
|
||||
<div class="form-row"><label>Status</label><select id="r-status"><option value="nacrt">Nacrt</option><option value="knjizen">Knjižen (auto-knjiženje)</option></select></div>
|
||||
<h4 style="font-size:11px;color:var(--t2);margin:14px 0 6px">STAVKE</h4>
|
||||
<div id="r-lines"></div>
|
||||
<button class="btn sec" onclick="addRacLine()" style="margin-top:8px">+ Dodaj stavku</button>
|
||||
<div id="r-summary" class="dnev-balans">Neto: 0,00 · PDV: 0,00 · Brutto: 0,00</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn sec" onclick="closeModal('m-rac')">Odustani</button>
|
||||
<button class="btn gold" onclick="saveRac()">Spremi račun</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-bg" id="m-zap" onclick="if(event.target===this)closeModal('m-zap')">
|
||||
<div class="modal">
|
||||
<h3>Novi zaposlenik</h3>
|
||||
<div class="form-row"><label>Ime</label><input id="z-ime"></div>
|
||||
<div class="form-row"><label>Prezime</label><input id="z-prezime"></div>
|
||||
<div class="form-row"><label>OIB</label><input id="z-oib" maxlength="11"></div>
|
||||
<div class="form-row"><label>Klub ID</label><input type="number" id="z-klub"></div>
|
||||
<div class="form-row"><label>Radno mjesto</label><input id="z-mjesto"></div>
|
||||
<div class="form-row"><label>Plata bruto</label><input type="number" step="0.01" id="z-bruto"></div>
|
||||
<div class="form-row"><label>IBAN</label><input id="z-iban"></div>
|
||||
<div class="form-actions">
|
||||
<button class="btn sec" onclick="closeModal('m-zap')">Odustani</button>
|
||||
<button class="btn gold" onclick="saveZap()">Spremi</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-bg" id="m-pl" onclick="if(event.target===this)closeModal('m-pl')">
|
||||
<div class="modal">
|
||||
<h3>Obračun plaće (HR 2026)</h3>
|
||||
<div class="form-row"><label>Zaposlenik</label><select id="pl-zap"></select></div>
|
||||
<div class="form-row"><label>Godina</label><input type="number" id="pl-god" value="2026"></div>
|
||||
<div class="form-row"><label>Mjesec</label><input type="number" id="pl-mj" min="1" max="12"></div>
|
||||
<div class="form-row"><label>Bruto (€)</label><input type="number" step="0.01" id="pl-bruto" placeholder="prazno = iz zaposlenika"></div>
|
||||
<div class="form-row"><label>Osobni odbitak</label><input type="number" step="0.01" id="pl-odb" value="600"></div>
|
||||
<div class="form-row"><label>Prirez %</label><input type="number" step="0.1" id="pl-prirez" value="0"></div>
|
||||
<div class="form-row"><label>Datum isplate</label><input type="date" id="pl-isplata"></div>
|
||||
<div class="form-row"><label>Knjiži u dnevnik</label><input type="checkbox" id="pl-knjizi"></div>
|
||||
<div class="form-actions">
|
||||
<button class="btn sec" onclick="closeModal('m-pl')">Odustani</button>
|
||||
<button class="btn gold" onclick="savePlaca()">Obračunaj</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API = '/api/v2/erp';
|
||||
const AUTH = () => ({ 'Authorization': 'Bearer ' + (localStorage.getItem('jwt') || localStorage.getItem('access_token') || 'admin-pgz-2026') });
|
||||
const fmt = n => (Number(n||0)).toLocaleString('hr-HR',{minimumFractionDigits:2,maximumFractionDigits:2});
|
||||
|
||||
async function api(path, opts={}) {
|
||||
const r = await fetch(API + path, { headers: { 'Content-Type':'application/json', ...AUTH() }, ...opts });
|
||||
if (!r.ok) {
|
||||
let detail = await r.text();
|
||||
throw new Error(`${r.status}: ${detail}`);
|
||||
}
|
||||
return r.json();
|
||||
}
|
||||
|
||||
// Tab switching
|
||||
document.querySelectorAll('.tab').forEach(t => t.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active'));
|
||||
document.querySelectorAll('.panel').forEach(x=>x.classList.remove('active'));
|
||||
t.classList.add('active');
|
||||
const panel = t.dataset.panel;
|
||||
document.getElementById('panel-' + panel).classList.add('active');
|
||||
loaders[panel] && loaders[panel]();
|
||||
}));
|
||||
|
||||
function closeModal(id){ document.getElementById(id).classList.remove('show'); }
|
||||
function openModal(id){ document.getElementById(id).classList.add('show'); }
|
||||
|
||||
// ===== KONTNI PLAN =====
|
||||
async function loadKontniPlan(){
|
||||
const q = document.getElementById('kp-q').value;
|
||||
const klasa = document.getElementById('kp-klasa').value;
|
||||
const vrsta = document.getElementById('kp-vrsta').value;
|
||||
const params = new URLSearchParams();
|
||||
if (q) params.set('q', q);
|
||||
if (klasa) params.set('klasa', klasa);
|
||||
if (vrsta) params.set('vrsta', vrsta);
|
||||
const d = await api('/kontni-plan?' + params.toString());
|
||||
const tbody = document.querySelector('#kp-tbl tbody');
|
||||
tbody.innerHTML = d.rows.map(r=>`<tr>
|
||||
<td><b>${r.sifra}</b></td><td>${r.naziv}</td><td>${r.klasa}</td><td>${r.vrsta}</td>
|
||||
<td>${r.aktivan?'✓':'✗'}</td>
|
||||
<td><button class="btn sec" onclick="event.stopPropagation();editKonto(${r.id})">✎</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
function openKontoModal(){
|
||||
['k-sifra','k-naziv','k-klasa'].forEach(i=>document.getElementById(i).value='');
|
||||
document.getElementById('m-konto').dataset.id='';
|
||||
openModal('m-konto');
|
||||
}
|
||||
async function editKonto(id){
|
||||
const d = await api('/kontni-plan?'); // we don't have a single GET, so fetch & filter
|
||||
const r = d.rows.find(x=>x.id===id);
|
||||
if(!r) return;
|
||||
document.getElementById('k-sifra').value=r.sifra;
|
||||
document.getElementById('k-naziv').value=r.naziv;
|
||||
document.getElementById('k-klasa').value=r.klasa;
|
||||
document.getElementById('k-vrsta').value=r.vrsta;
|
||||
document.getElementById('m-konto').dataset.id=id;
|
||||
openModal('m-konto');
|
||||
}
|
||||
async function saveKonto(){
|
||||
const body = {
|
||||
sifra: document.getElementById('k-sifra').value.trim(),
|
||||
naziv: document.getElementById('k-naziv').value.trim(),
|
||||
klasa: parseInt(document.getElementById('k-klasa').value),
|
||||
vrsta: document.getElementById('k-vrsta').value,
|
||||
aktivan: true
|
||||
};
|
||||
const id = document.getElementById('m-konto').dataset.id;
|
||||
try {
|
||||
if(id) await api('/kontni-plan/'+id, { method:'PUT', body: JSON.stringify(body) });
|
||||
else await api('/kontni-plan', { method:'POST', body: JSON.stringify(body) });
|
||||
closeModal('m-konto'); loadKontniPlan();
|
||||
} catch(e){ alert(e.message); }
|
||||
}
|
||||
|
||||
// ===== PARTNERI =====
|
||||
async function loadPartneri(){
|
||||
const q = document.getElementById('part-q').value;
|
||||
const v = document.getElementById('part-vrsta').value;
|
||||
const p = new URLSearchParams();
|
||||
if(q)p.set('q',q); if(v)p.set('vrsta',v);
|
||||
const d = await api('/partneri?'+p.toString());
|
||||
const tbody = document.querySelector('#part-tbl tbody');
|
||||
tbody.innerHTML = d.rows.map(r=>`<tr onclick="loadPartnerSaldo(${r.id})">
|
||||
<td>${r.id}</td><td>${r.oib||'—'}</td><td><b>${r.naziv}</b></td>
|
||||
<td>${r.vrsta}</td><td>${r.grad||''}</td><td>${r.iban||''}</td>
|
||||
<td class="num" id="ps-${r.id}">…</td>
|
||||
<td><button class="btn sec" onclick="event.stopPropagation();editPartner(${r.id})">✎</button></td>
|
||||
</tr>`).join('');
|
||||
d.rows.forEach(r => {
|
||||
api('/partneri/'+r.id+'/saldo').then(x => {
|
||||
const el = document.getElementById('ps-'+r.id);
|
||||
if(el) el.textContent = fmt(x.info?.saldo || 0);
|
||||
}).catch(()=>{});
|
||||
});
|
||||
}
|
||||
function openPartnerModal(){
|
||||
['p-naziv','p-oib','p-iban','p-adresa','p-grad','p-email','p-telefon'].forEach(i=>document.getElementById(i).value='');
|
||||
document.getElementById('m-partner').dataset.id='';
|
||||
openModal('m-partner');
|
||||
}
|
||||
async function editPartner(id){
|
||||
const d = await api('/partneri?');
|
||||
const r = d.rows.find(x=>x.id===id);
|
||||
if(!r) return;
|
||||
document.getElementById('p-naziv').value=r.naziv||'';
|
||||
document.getElementById('p-oib').value=r.oib||'';
|
||||
document.getElementById('p-vrsta').value=r.vrsta;
|
||||
document.getElementById('p-iban').value=r.iban||'';
|
||||
document.getElementById('p-adresa').value=r.adresa||'';
|
||||
document.getElementById('p-grad').value=r.grad||'';
|
||||
document.getElementById('p-email').value=r.email||'';
|
||||
document.getElementById('p-telefon').value=r.telefon||'';
|
||||
document.getElementById('m-partner').dataset.id=id;
|
||||
openModal('m-partner');
|
||||
}
|
||||
async function savePartner(){
|
||||
const body = {
|
||||
naziv: document.getElementById('p-naziv').value.trim(),
|
||||
oib: document.getElementById('p-oib').value.trim() || null,
|
||||
vrsta: document.getElementById('p-vrsta').value,
|
||||
iban: document.getElementById('p-iban').value.trim() || null,
|
||||
adresa: document.getElementById('p-adresa').value || null,
|
||||
grad: document.getElementById('p-grad').value || null,
|
||||
email: document.getElementById('p-email').value || null,
|
||||
telefon: document.getElementById('p-telefon').value || null,
|
||||
};
|
||||
const id = document.getElementById('m-partner').dataset.id;
|
||||
try {
|
||||
if(id) await api('/partneri/'+id, { method:'PUT', body: JSON.stringify(body) });
|
||||
else await api('/partneri', { method:'POST', body: JSON.stringify(body) });
|
||||
closeModal('m-partner'); loadPartneri();
|
||||
} catch(e){ alert(e.message); }
|
||||
}
|
||||
async function loadPartnerSaldo(id){
|
||||
const d = await api('/partneri/'+id+'/saldo');
|
||||
alert(`Partner saldo: ${fmt(d.info?.saldo||0)} EUR\nDuguje: ${fmt(d.info?.uk_duguje||0)}\nPotražuje: ${fmt(d.info?.uk_potrazuje||0)}\nBroj stavki: ${d.stavke.length}`);
|
||||
}
|
||||
|
||||
// ===== DNEVNIK =====
|
||||
async function loadDnevnik(){
|
||||
const g = document.getElementById('dnev-godina').value;
|
||||
const t = document.getElementById('dnev-tip').value;
|
||||
const p = new URLSearchParams();
|
||||
if(g) p.set('godina', g);
|
||||
if(t) p.set('dokument_tip', t);
|
||||
const d = await api('/dnevnik?'+p.toString());
|
||||
const tbody = document.querySelector('#dnev-tbl tbody');
|
||||
tbody.innerHTML = d.rows.map(r=>`<tr onclick="dnevDetail(${r.id})">
|
||||
<td>${r.redni_broj||r.id}</td><td>${r.datum}</td><td>${r.opis||''}</td>
|
||||
<td>${r.dokument_tip||''}</td><td class="num">${r.broj_stavki}</td>
|
||||
<td class="num">${fmt(r.uk_duguje)}</td>
|
||||
<td><button class="btn sec" onclick="event.stopPropagation();dnevStorno(${r.id})">↺ Storno</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
async function dnevDetail(id){
|
||||
const d = await api('/dnevnik/'+id);
|
||||
let html = `<b>${d.head.opis||''}</b> · ${d.head.datum} · #${d.head.id}\n\nSTAVKE:\n`;
|
||||
d.stavke.forEach(s => html += `${s.konto_sifra} ${s.konto_naziv}: D=${fmt(s.duguje)} P=${fmt(s.potrazuje)}\n`);
|
||||
alert(html);
|
||||
}
|
||||
async function dnevStorno(id){
|
||||
if(!confirm('Sigurno storno za zapis #'+id+'?')) return;
|
||||
try {
|
||||
await api('/dnevnik/'+id+'/storno', { method:'POST' });
|
||||
loadDnevnik();
|
||||
} catch(e){ alert(e.message); }
|
||||
}
|
||||
let kontoCache = [];
|
||||
async function loadKontoCache(){
|
||||
if(kontoCache.length) return kontoCache;
|
||||
const d = await api('/kontni-plan?aktivan=true');
|
||||
kontoCache = d.rows;
|
||||
return kontoCache;
|
||||
}
|
||||
async function openDnevnikModal(){
|
||||
await loadKontoCache();
|
||||
document.getElementById('d-datum').value = new Date().toISOString().slice(0,10);
|
||||
document.getElementById('d-opis').value = '';
|
||||
document.getElementById('d-tip').value = 'rucno';
|
||||
document.getElementById('d-lines').innerHTML = '';
|
||||
addDnevLine(); addDnevLine();
|
||||
updateDnevBalans();
|
||||
openModal('m-dnev');
|
||||
}
|
||||
function addDnevLine(){
|
||||
const opts = kontoCache.map(k=>`<option value="${k.id}">${k.sifra} — ${k.naziv}</option>`).join('');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'dnev-line-row';
|
||||
div.innerHTML = `
|
||||
<select class="d-konto">${opts}</select>
|
||||
<input class="d-opis" placeholder="opis stavke">
|
||||
<input type="number" step="0.01" class="d-duguje" placeholder="Duguje" oninput="updateDnevBalans()">
|
||||
<input type="number" step="0.01" class="d-potrazuje" placeholder="Potražuje" oninput="updateDnevBalans()">
|
||||
<input type="number" class="d-partner" placeholder="Partner ID (opc)">
|
||||
<button class="btn red" onclick="this.parentElement.remove();updateDnevBalans()">×</button>`;
|
||||
document.getElementById('d-lines').appendChild(div);
|
||||
}
|
||||
function updateDnevBalans(){
|
||||
const rows = document.querySelectorAll('#d-lines .dnev-line-row');
|
||||
let d=0,p=0;
|
||||
rows.forEach(r => {
|
||||
d += parseFloat(r.querySelector('.d-duguje').value)||0;
|
||||
p += parseFloat(r.querySelector('.d-potrazuje').value)||0;
|
||||
});
|
||||
const el = document.getElementById('d-balans');
|
||||
el.textContent = `Duguje: ${fmt(d)} · Potražuje: ${fmt(p)} · Razlika: ${fmt(d-p)}`;
|
||||
el.className = 'dnev-balans ' + (d===p && d>0 ? 'ok' : 'bad');
|
||||
}
|
||||
async function saveDnev(){
|
||||
const stavke = Array.from(document.querySelectorAll('#d-lines .dnev-line-row')).map(r => ({
|
||||
konto_id: parseInt(r.querySelector('.d-konto').value),
|
||||
opis: r.querySelector('.d-opis').value,
|
||||
duguje: parseFloat(r.querySelector('.d-duguje').value)||0,
|
||||
potrazuje: parseFloat(r.querySelector('.d-potrazuje').value)||0,
|
||||
partner_id: parseInt(r.querySelector('.d-partner').value)||null
|
||||
}));
|
||||
const body = {
|
||||
datum: document.getElementById('d-datum').value,
|
||||
opis: document.getElementById('d-opis').value,
|
||||
dokument_tip: document.getElementById('d-tip').value,
|
||||
stavke
|
||||
};
|
||||
try {
|
||||
await api('/dnevnik', { method:'POST', body: JSON.stringify(body) });
|
||||
closeModal('m-dnev'); loadDnevnik();
|
||||
} catch(e){ alert(e.message); }
|
||||
}
|
||||
|
||||
// ===== GLAVNA KNJIGA =====
|
||||
async function loadGlavnaKnjiga(){
|
||||
const k = document.getElementById('gk-klasa').value;
|
||||
const p = new URLSearchParams();
|
||||
if(k) p.set('klasa', k);
|
||||
const d = await api('/glavna-knjiga?'+p.toString());
|
||||
const tbody = document.querySelector('#gk-tbl tbody');
|
||||
tbody.innerHTML = d.rows.map(r=>`<tr>
|
||||
<td><b>${r.sifra}</b></td><td>${r.naziv}</td><td>${r.klasa}</td><td>${r.vrsta}</td>
|
||||
<td class="num">${fmt(r.sum_duguje)}</td>
|
||||
<td class="num">${fmt(r.sum_potrazuje)}</td>
|
||||
<td class="num"><b>${fmt(r.saldo)}</b></td>
|
||||
<td class="num">${r.broj_stavki}</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
// ===== RAČUNI =====
|
||||
let partnerCache = [];
|
||||
async function loadPartnerCache(){
|
||||
if(partnerCache.length) return partnerCache;
|
||||
const d = await api('/partneri?');
|
||||
partnerCache = d.rows;
|
||||
return partnerCache;
|
||||
}
|
||||
async function loadRacuni(){
|
||||
const tip = document.getElementById('rac-tip').value;
|
||||
const status = document.getElementById('rac-status').value;
|
||||
const godina = document.getElementById('rac-godina').value;
|
||||
const p = new URLSearchParams();
|
||||
if(status) p.set('status', status);
|
||||
if(godina) p.set('godina', godina);
|
||||
const d = await api('/racuni/'+tip+'?'+p.toString());
|
||||
const tbody = document.querySelector('#rac-tbl tbody');
|
||||
tbody.innerHTML = d.rows.map(r=>`<tr onclick="racDetail('${tip}',${r.id})">
|
||||
<td>${r.id}</td><td>${r.broj||''}</td><td>${r.datum_izdavanja}</td>
|
||||
<td>${r.partner_naziv||''}</td><td>${r.partner_oib||''}</td>
|
||||
<td class="num">${fmt(r.iznos_neto)}</td>
|
||||
<td class="num">${fmt(r.iznos_pdv)}</td>
|
||||
<td class="num"><b>${fmt(r.iznos_brutto)}</b></td>
|
||||
<td><span class="badge ${r.status}">${r.status}</span></td>
|
||||
<td>${r.status==='nacrt'?`<button class="btn green" onclick="event.stopPropagation();knjizi('${tip}',${r.id})">Knjiži</button>`:''}</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
async function knjizi(tip, id){
|
||||
if(!confirm('Knjižiti račun #'+id+' u dnevnik?')) return;
|
||||
try{ await api('/racuni/'+tip+'/'+id+'/knjizi', {method:'POST'}); loadRacuni(); }
|
||||
catch(e){ alert(e.message); }
|
||||
}
|
||||
async function racDetail(tip, id){
|
||||
const d = await api('/racuni/'+tip+'/'+id);
|
||||
let html = `Račun ${d.head.broj||''} · ${d.head.partner_naziv}\nNeto: ${fmt(d.head.iznos_neto)} · PDV: ${fmt(d.head.iznos_pdv)} · Brutto: ${fmt(d.head.iznos_brutto)}\n\nSTAVKE:\n`;
|
||||
d.stavke.forEach(s => html += `${s.naziv} ${s.kolicina}×${fmt(s.cijena_jed)} = ${fmt(s.iznos_brutto)}\n`);
|
||||
alert(html);
|
||||
}
|
||||
async function openRacunModal(tip){
|
||||
await loadPartnerCache();
|
||||
document.getElementById('m-rac').dataset.tip = tip;
|
||||
document.getElementById('m-rac-title').textContent = 'Novi ' + tip + ' račun';
|
||||
document.getElementById('r-broj').value='';
|
||||
document.getElementById('r-datum').value = new Date().toISOString().slice(0,10);
|
||||
document.getElementById('r-dospjece').value = '';
|
||||
document.getElementById('r-status').value = 'nacrt';
|
||||
document.getElementById('r-partner').innerHTML = partnerCache.map(p=>`<option value="${p.id}">${p.naziv}${p.oib?' · '+p.oib:''}</option>`).join('');
|
||||
document.getElementById('r-lines').innerHTML='';
|
||||
addRacLine();
|
||||
updateRacSummary();
|
||||
openModal('m-rac');
|
||||
}
|
||||
function addRacLine(){
|
||||
const div = document.createElement('div');
|
||||
div.className = 'dnev-line-row';
|
||||
div.style.gridTemplateColumns = '1fr 60px 80px 80px 60px 30px';
|
||||
div.innerHTML = `
|
||||
<input class="r-naziv" placeholder="Naziv stavke">
|
||||
<input type="number" step="0.01" class="r-kol" value="1" oninput="updateRacSummary()">
|
||||
<input type="number" step="0.01" class="r-cij" placeholder="Cijena" oninput="updateRacSummary()">
|
||||
<input type="number" step="0.01" class="r-pop" value="0" placeholder="Popust %" oninput="updateRacSummary()">
|
||||
<input type="number" step="0.01" class="r-pdv" value="25" placeholder="PDV %" oninput="updateRacSummary()">
|
||||
<button class="btn red" onclick="this.parentElement.remove();updateRacSummary()">×</button>`;
|
||||
document.getElementById('r-lines').appendChild(div);
|
||||
}
|
||||
function updateRacSummary(){
|
||||
const rows = document.querySelectorAll('#r-lines .dnev-line-row');
|
||||
let neto=0, pdv=0;
|
||||
rows.forEach(r => {
|
||||
const k = parseFloat(r.querySelector('.r-kol').value)||0;
|
||||
const c = parseFloat(r.querySelector('.r-cij').value)||0;
|
||||
const pop = parseFloat(r.querySelector('.r-pop').value)||0;
|
||||
const ppdv = parseFloat(r.querySelector('.r-pdv').value)||0;
|
||||
const n = k*c*(1-pop/100);
|
||||
neto += n;
|
||||
pdv += n*ppdv/100;
|
||||
});
|
||||
document.getElementById('r-summary').textContent = `Neto: ${fmt(neto)} · PDV: ${fmt(pdv)} · Brutto: ${fmt(neto+pdv)}`;
|
||||
}
|
||||
async function saveRac(){
|
||||
const tip = document.getElementById('m-rac').dataset.tip;
|
||||
const stavke = Array.from(document.querySelectorAll('#r-lines .dnev-line-row')).map(r => ({
|
||||
naziv: r.querySelector('.r-naziv').value,
|
||||
kolicina: parseFloat(r.querySelector('.r-kol').value)||1,
|
||||
cijena_jed: parseFloat(r.querySelector('.r-cij').value)||0,
|
||||
popust_pct: parseFloat(r.querySelector('.r-pop').value)||0,
|
||||
pdv_pct: parseFloat(r.querySelector('.r-pdv').value)||25,
|
||||
}));
|
||||
const body = {
|
||||
broj: document.getElementById('r-broj').value || null,
|
||||
partner_id: parseInt(document.getElementById('r-partner').value),
|
||||
datum_izdavanja: document.getElementById('r-datum').value,
|
||||
datum_dospjeca: document.getElementById('r-dospjece').value || null,
|
||||
status: document.getElementById('r-status').value,
|
||||
stavke
|
||||
};
|
||||
try {
|
||||
await api('/racuni/'+tip, { method:'POST', body: JSON.stringify(body) });
|
||||
closeModal('m-rac');
|
||||
loadRacuni();
|
||||
} catch(e){ alert(e.message); }
|
||||
}
|
||||
async function importERacun(){
|
||||
const f = document.getElementById('eracun-file').files[0];
|
||||
if(!f) return;
|
||||
const fd = new FormData();
|
||||
fd.append('file', f);
|
||||
const r = await fetch(API+'/racuni/eracun-import', { method:'POST', headers: AUTH(), body: fd });
|
||||
const d = await r.json();
|
||||
alert(r.ok ? `e-Račun importiran:\nBroj: ${d.broj}\nNeto: ${fmt(d.neto)} PDV: ${fmt(d.pdv)} Brutto: ${fmt(d.brutto)}` : 'Greška: '+JSON.stringify(d));
|
||||
document.getElementById('eracun-file').value='';
|
||||
loadRacuni();
|
||||
}
|
||||
|
||||
// ===== PDV =====
|
||||
async function loadPdv(){
|
||||
const g = document.getElementById('pdv-godina').value;
|
||||
const m = document.getElementById('pdv-mjesec').value;
|
||||
const params = new URLSearchParams({ godina:g });
|
||||
if(m) params.set('mjesec', m);
|
||||
const [u,i,o] = await Promise.all([
|
||||
api('/pdv/knjiga-u?'+params.toString()),
|
||||
api('/pdv/knjiga-i?'+params.toString()),
|
||||
api('/pdv/obrazac?'+params.toString())
|
||||
]);
|
||||
document.querySelector('#pdv-u-tbl tbody').innerHTML = u.rows.map(r=>`<tr><td>${r.broj||''}</td><td>${r.datum_izdavanja}</td><td>${r.partner_naziv||''}</td><td>${r.partner_oib||''}</td><td class="num">${fmt(r.iznos_neto)}</td><td class="num">${fmt(r.iznos_pdv)}</td></tr>`).join('');
|
||||
document.querySelector('#pdv-i-tbl tbody').innerHTML = i.rows.map(r=>`<tr><td>${r.broj||''}</td><td>${r.datum_izdavanja}</td><td>${r.partner_naziv||''}</td><td>${r.partner_oib||''}</td><td class="num">${fmt(r.iznos_neto)}</td><td class="num">${fmt(r.iznos_pdv)}</td></tr>`).join('');
|
||||
document.getElementById('pdv-summary').innerHTML = `
|
||||
<div class="kpi"><div class="kpi-l">Pretporez (U)</div><div class="kpi-v">${fmt(o.ulazni.pdv)} €</div></div>
|
||||
<div class="kpi"><div class="kpi-l">PDV obveza (I)</div><div class="kpi-v">${fmt(o.izlazni.pdv)} €</div></div>
|
||||
<div class="kpi ${o.obveza_za_uplatu>0?'r':'g'}"><div class="kpi-l">Obveza za uplatu</div><div class="kpi-v">${fmt(o.obveza_za_uplatu)} €</div></div>
|
||||
<div class="kpi g"><div class="kpi-l">Pretporez za povrat</div><div class="kpi-v">${fmt(o.pretporez_za_povrat)} €</div></div>`;
|
||||
}
|
||||
|
||||
// ===== ZAPOSLENICI + PLAĆE =====
|
||||
async function loadZap(){
|
||||
const d = await api('/zaposlenici');
|
||||
document.querySelector('#zap-tbl tbody').innerHTML = d.rows.map(r=>`<tr>
|
||||
<td>${r.id}</td><td>${r.oib||'—'}</td><td>${r.ime}</td><td>${r.prezime}</td>
|
||||
<td>${r.klub_naziv||'—'}</td><td>${r.radno_mjesto||''}</td>
|
||||
<td class="num">${fmt(r.plata_bruto)}</td><td>${r.aktivan?'✓':'✗'}</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
async function loadPlace(){
|
||||
const g = document.getElementById('pl-godina').value;
|
||||
const m = document.getElementById('pl-mjesec').value;
|
||||
const p = new URLSearchParams();
|
||||
if(g)p.set('godina',g); if(m)p.set('mjesec',m);
|
||||
const d = await api('/place/obracun?'+p.toString());
|
||||
document.querySelector('#pl-tbl tbody').innerHTML = d.rows.map(r=>`<tr>
|
||||
<td>${r.id}</td><td>${r.ime} ${r.prezime}</td><td>${r.godina}/${String(r.mjesec).padStart(2,'0')}</td>
|
||||
<td class="num">${fmt(r.bruto)}</td>
|
||||
<td class="num">${fmt(r.doprinosi_iz_plate)}</td>
|
||||
<td class="num">${fmt(r.dohodnina)}</td>
|
||||
<td class="num"><b>${fmt(r.neto)}</b></td>
|
||||
<td class="num">${fmt(r.doprinosi_na_plate)}</td>
|
||||
<td class="num">${fmt(r.ukupni_trosak)}</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
function openZapModal(){
|
||||
['z-ime','z-prezime','z-oib','z-klub','z-mjesto','z-bruto','z-iban'].forEach(i=>document.getElementById(i).value='');
|
||||
openModal('m-zap');
|
||||
}
|
||||
async function saveZap(){
|
||||
const body = {
|
||||
ime: document.getElementById('z-ime').value,
|
||||
prezime: document.getElementById('z-prezime').value,
|
||||
oib: document.getElementById('z-oib').value || null,
|
||||
klub_id: parseInt(document.getElementById('z-klub').value)||null,
|
||||
radno_mjesto: document.getElementById('z-mjesto').value || null,
|
||||
plata_bruto: parseFloat(document.getElementById('z-bruto').value)||0,
|
||||
iban: document.getElementById('z-iban').value || null,
|
||||
aktivan: true
|
||||
};
|
||||
try { await api('/zaposlenici', { method:'POST', body: JSON.stringify(body) }); closeModal('m-zap'); loadZap(); }
|
||||
catch(e){ alert(e.message); }
|
||||
}
|
||||
async function openPlacaModal(){
|
||||
const d = await api('/zaposlenici');
|
||||
document.getElementById('pl-zap').innerHTML = d.rows.map(r=>`<option value="${r.id}">${r.ime} ${r.prezime}</option>`).join('');
|
||||
openModal('m-pl');
|
||||
}
|
||||
async function savePlaca(){
|
||||
const body = {
|
||||
zaposlenik_id: parseInt(document.getElementById('pl-zap').value),
|
||||
godina: parseInt(document.getElementById('pl-god').value),
|
||||
mjesec: parseInt(document.getElementById('pl-mj').value),
|
||||
bruto: parseFloat(document.getElementById('pl-bruto').value) || null,
|
||||
osobni_odbitak: parseFloat(document.getElementById('pl-odb').value)||600,
|
||||
prirez_pct: parseFloat(document.getElementById('pl-prirez').value)||0,
|
||||
datum_isplate: document.getElementById('pl-isplata').value || null,
|
||||
knjizi: document.getElementById('pl-knjizi').checked
|
||||
};
|
||||
try {
|
||||
const r = await api('/place/obracun', { method:'POST', body: JSON.stringify(body) });
|
||||
alert(`Bruto: ${fmt(r.calc.bruto)}\nDoprinosi iz: ${fmt(r.calc.doprinosi_iz_plate)}\nDohodnina: ${fmt(r.calc.dohodnina)}\nNeto: ${fmt(r.calc.neto)}\nUkupni trošak: ${fmt(r.calc.ukupni_trosak)}`);
|
||||
closeModal('m-pl'); loadPlace();
|
||||
} catch(e){ alert(e.message); }
|
||||
}
|
||||
|
||||
// ===== PRORAČUN =====
|
||||
async function loadProracun(){
|
||||
const d = await api('/proracun');
|
||||
document.querySelector('#pr-tbl tbody').innerHTML = d.rows.map(r=>`<tr>
|
||||
<td><b>${r.godina}</b></td><td class="num">${fmt(r.proracun_pgz)}</td>
|
||||
<td class="num">${fmt(r.rebalans1)}</td><td class="num">${fmt(r.rebalans2)}</td>
|
||||
<td class="num"><b>${fmt(r.ukupno_pgz)}</b></td><td class="num">${fmt(r.ministarstvo)}</td>
|
||||
<td class="num"><b>${fmt(r.ukupno)}</b></td><td>${r.napomena||''}</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
// ===== IZVJEŠTAJI =====
|
||||
async function loadIzvjestaj(){
|
||||
const tip = document.getElementById('iz-tip').value;
|
||||
const g = document.getElementById('iz-godina').value;
|
||||
const d = await api('/izvjestaji/'+tip+'?godina='+g);
|
||||
const out = document.getElementById('iz-out');
|
||||
if(tip==='bilanca'){
|
||||
out.innerHTML = `
|
||||
<div class="kpi-grid">
|
||||
<div class="kpi g"><div class="kpi-l">Ukupno aktiva</div><div class="kpi-v">${fmt(d.ukupno_aktiva)} €</div></div>
|
||||
<div class="kpi"><div class="kpi-l">Ukupno pasiva</div><div class="kpi-v">${fmt(d.ukupno_pasiva)} €</div></div>
|
||||
<div class="kpi ${d.balans_ok?'g':'r'}"><div class="kpi-l">Balans</div><div class="kpi-v">${d.balans_ok?'✓ OK':'✗ neusklađen'}</div></div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
|
||||
<div><h4 style="font-size:12px;color:var(--t2);margin-bottom:6px">AKTIVA</h4>
|
||||
<div class="tbl-wrap"><table><thead><tr><th>Šifra</th><th>Naziv</th><th class="num">Saldo</th></tr></thead>
|
||||
<tbody>${d.aktiva.map(r=>`<tr><td>${r.sifra}</td><td>${r.naziv}</td><td class="num">${fmt(r.saldo)}</td></tr>`).join('')}</tbody></table></div></div>
|
||||
<div><h4 style="font-size:12px;color:var(--t2);margin-bottom:6px">PASIVA</h4>
|
||||
<div class="tbl-wrap"><table><thead><tr><th>Šifra</th><th>Naziv</th><th class="num">Saldo</th></tr></thead>
|
||||
<tbody>${d.pasiva.map(r=>`<tr><td>${r.sifra}</td><td>${r.naziv}</td><td class="num">${fmt(r.saldo)}</td></tr>`).join('')}</tbody></table></div></div>
|
||||
</div>`;
|
||||
} else if(tip==='pnl'){
|
||||
out.innerHTML = `
|
||||
<div class="kpi-grid">
|
||||
<div class="kpi g"><div class="kpi-l">Prihodi</div><div class="kpi-v">${fmt(d.ukupno_prihodi)} €</div></div>
|
||||
<div class="kpi r"><div class="kpi-l">Rashodi</div><div class="kpi-v">${fmt(d.ukupno_rashodi)} €</div></div>
|
||||
<div class="kpi ${d.rezultat>=0?'g':'r'}"><div class="kpi-l">Rezultat (${d.tip_rezultata})</div><div class="kpi-v">${fmt(d.rezultat)} €</div></div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
|
||||
<div><h4 style="font-size:12px;color:var(--t2);margin-bottom:6px">PRIHODI</h4>
|
||||
<div class="tbl-wrap"><table><thead><tr><th>Šifra</th><th>Naziv</th><th class="num">Iznos</th></tr></thead>
|
||||
<tbody>${d.prihodi.map(r=>`<tr><td>${r.sifra}</td><td>${r.naziv}</td><td class="num">${fmt(r.iznos)}</td></tr>`).join('')}</tbody></table></div></div>
|
||||
<div><h4 style="font-size:12px;color:var(--t2);margin-bottom:6px">RASHODI</h4>
|
||||
<div class="tbl-wrap"><table><thead><tr><th>Šifra</th><th>Naziv</th><th class="num">Iznos</th></tr></thead>
|
||||
<tbody>${d.rashodi.map(r=>`<tr><td>${r.sifra}</td><td>${r.naziv}</td><td class="num">${fmt(r.iznos)}</td></tr>`).join('')}</tbody></table></div></div>
|
||||
</div>`;
|
||||
} else {
|
||||
out.innerHTML = `
|
||||
<div class="tbl-wrap"><table><thead><tr><th>Mjesec</th><th class="num">Uplate</th><th class="num">Isplate</th><th class="num">Net</th></tr></thead>
|
||||
<tbody>${d.po_mjesecu.map(r=>`<tr><td>${r.mjesec}</td><td class="num">${fmt(r.uplate)}</td><td class="num">${fmt(r.isplate)}</td><td class="num"><b>${fmt(Number(r.uplate)-Number(r.isplate))}</b></td></tr>`).join('')}</tbody></table></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function exportXlsx(report, godina, mjesec){
|
||||
let url = API+'/export/xlsx/'+report+'?godina='+godina;
|
||||
if(mjesec) url += '&mjesec='+mjesec;
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
function exportPdf(report, godina){
|
||||
window.open(API+'/export/pdf/'+report+'?godina='+godina, '_blank');
|
||||
}
|
||||
|
||||
// Lazy loaders per panel
|
||||
const loaders = {
|
||||
dnevnik: loadDnevnik,
|
||||
glavna: loadGlavnaKnjiga,
|
||||
partneri: loadPartneri,
|
||||
racuni: loadRacuni,
|
||||
pdv: loadPdv,
|
||||
place: () => { loadZap(); loadPlace(); },
|
||||
proracun: loadProracun,
|
||||
izvjestaji: loadIzvjestaj,
|
||||
kontni: loadKontniPlan
|
||||
};
|
||||
|
||||
// Initial
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadKontoCache();
|
||||
loadPartnerCache();
|
||||
loadDnevnik();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -25,7 +25,8 @@
|
||||
{id:'savezi', ic:'\u{1F3C5}', label:'Savezi', href:'/static/sport2.html#savezi'},
|
||||
{id:'klubovi', ic:'⬢', label:'Klubovi', href:'/static/sport2.html#klubovi'},
|
||||
{id:'sportasi', ic:'\u{1F464}', label:'Sportaši', href:'/static/sport2.html#sportasi'},
|
||||
{id:'manifestacije', ic:'\u{1F4C5}', label:'Manifestacije', href:'/static/sport2.html#manifestacije'}
|
||||
{id:'manifestacije', ic:'\u{1F4C5}', label:'Manifestacije', href:'/static/sport2.html#manifestacije'},
|
||||
{id:'dokumentilib', ic:'\u{1F4DA}', label:'Dokumenti', href:'/sport/dokumenti'}
|
||||
]},
|
||||
{title:'OPERATIVA', items: [
|
||||
{id:'profil', ic:'\u{1F464}', label:'Moj profil', href:'/app#profil'},
|
||||
@@ -53,10 +54,12 @@
|
||||
{id:'audit', ic:'\u{1F512}', label:'Audit log', href:'/audit'}
|
||||
]},
|
||||
{title:'ADMIN', requireRole:['pgz_admin','super_admin'], items: [
|
||||
{id:'korisnici', ic:'\u{1F465}', label:'Korisnici', href:'/admin#korisnici'},
|
||||
{id:'tenanti', ic:'\u{1F3E2}', label:'Tenanti', href:'/admin#tenanti'},
|
||||
{id:'sigurnost', ic:'\u{1F6E1}', label:'Sigurnost', href:'/admin#sigurnost'},
|
||||
{id:'sustav', ic:'⚙', label:'Sustav', href:'/admin#sustav'}
|
||||
{id:'users', ic:'\u{1F465}', label:'Korisnici', href:'/admin/users#users'},
|
||||
{id:'tenants', ic:'\u{1F3E2}', label:'Tenanti', href:'/admin/users#tenants'},
|
||||
{id:'security', ic:'\u{1F6E1}', label:'Sigurnost', href:'/admin/users#security'},
|
||||
{id:'rbac', ic:'\u{1F511}', label:'RBAC matrica', href:'/admin/users#rbac'},
|
||||
{id:'audit', ic:'\u{1F512}', label:'Audit log', href:'/admin/users#audit'},
|
||||
{id:'gdpr', ic:'\u{1F512}', label:'GDPR', href:'/admin/users#gdpr'}
|
||||
]}
|
||||
];
|
||||
|
||||
|
||||
+265
-15
@@ -288,6 +288,7 @@ a.tag:hover,.tag[onclick]:hover{transform:translateY(-1px);filter:brightness(1.1
|
||||
<section id="pg-savezi" class="section"></section>
|
||||
<section id="pg-klubovi" class="section"></section>
|
||||
<section id="pg-sportasi" class="section"></section>
|
||||
<section id="pg-igraci-kat" class="section"></section>
|
||||
<section id="pg-financije" class="section"></section>
|
||||
<section id="pg-objekti" class="section"></section>
|
||||
<section id="pg-manifestacije" class="section"></section>
|
||||
@@ -315,6 +316,7 @@ const NAV_ITEMS = [
|
||||
{id:'savezi', ic:'\u{1F3C5}', label:'Savezi'},
|
||||
{id:'klubovi', ic:'⬢', label:'Klubovi'},
|
||||
{id:'sportasi', ic:'\u{1F464}', label:'Sportaši'},
|
||||
{id:'igraci-kat', ic:'\u{1F3F7}', label:'Po kategoriji'},
|
||||
{id:'financije', ic:'€', label:'Financije'},
|
||||
{id:'objekti', ic:'\u{1F4CD}', label:'Objekti'},
|
||||
{id:'manifestacije', ic:'\u{1F4C5}', label:'Manifestacije'},
|
||||
@@ -327,6 +329,7 @@ const SECTION_TITLES = {
|
||||
savezi: ['Savezi', '246 sportskih saveza'],
|
||||
klubovi: ['Klubovi', 'Sportski klubovi PGŽ'],
|
||||
sportasi: ['Sportaši', 'Registrirani članovi'],
|
||||
'igraci-kat': ['Igrači po kategoriji', 'Grupirani po dobnoj/natjecateljskoj kategoriji'],
|
||||
financije: ['Financije', 'Sufinanciranje sporta'],
|
||||
objekti: ['Sportski objekti', 'Geocodirana infrastruktura'],
|
||||
manifestacije: ['Manifestacije', 'Sportski događaji'],
|
||||
@@ -628,6 +631,7 @@ function setSort(section, key){
|
||||
case 'savezi': return applySaveziFilter();
|
||||
case 'klubovi': return applyKluboviFilter();
|
||||
case 'sportasi': return applySportasiFilter();
|
||||
case 'igraci-kat': return applyIgraciKatFilter && applyIgraciKatFilter();
|
||||
case 'objekti': return applyObjektiFilter();
|
||||
case 'manifestacije': return applyManifFilter();
|
||||
case 'financije': return refreshFinancije();
|
||||
@@ -710,6 +714,7 @@ function loadSection(id){
|
||||
case 'savezi': return loadSavezi();
|
||||
case 'klubovi': return loadKlubovi();
|
||||
case 'sportasi': return loadSportasi();
|
||||
case 'igraci-kat': return loadIgraciKat();
|
||||
case 'financije': return loadFinancije();
|
||||
case 'objekti': return loadObjekti();
|
||||
case 'manifestacije': return loadManifestacije();
|
||||
@@ -1133,9 +1138,16 @@ async function loadSavezi(){
|
||||
}
|
||||
function renderSaveziShell(){
|
||||
const root = $('#pg-savezi');
|
||||
const sports = Array.from(new Set((_cache.savezi||[]).map(s=>s.sport).filter(Boolean))).sort();
|
||||
root.innerHTML = `
|
||||
<div class="toolbar">
|
||||
<input type="search" id="sav-q" placeholder="🔍 Pretraži savez…">
|
||||
<select id="sav-sport"><option value="">Svi sportovi</option>${sports.map(s=>'<option value="'+esc(s)+'">'+esc(s)+'</option>').join('')}</select>
|
||||
<select id="sav-kat">
|
||||
<option value="">Sve razine</option>
|
||||
<option value="zupanijski">Županijski</option>
|
||||
<option value="gradski">Gradski</option>
|
||||
</select>
|
||||
<select id="sav-pgz">
|
||||
<option value="">Svi savezi</option>
|
||||
<option value="1">Samo PGŽ relevantni</option>
|
||||
@@ -1149,6 +1161,8 @@ function renderSaveziShell(){
|
||||
<div id="sav-out"></div>
|
||||
`;
|
||||
$('#sav-q').addEventListener('input', debounce(applySaveziFilter, 200));
|
||||
$('#sav-sport').addEventListener('change', applySaveziFilter);
|
||||
$('#sav-kat').addEventListener('change', applySaveziFilter);
|
||||
$('#sav-pgz').addEventListener('change', applySaveziFilter);
|
||||
}
|
||||
function setSaveziView(v){
|
||||
@@ -1160,8 +1174,13 @@ function setSaveziView(v){
|
||||
function applySaveziFilter(){
|
||||
const q = (($('#sav-q')?$('#sav-q').value:'') || '').toLowerCase().trim();
|
||||
const pgz = $('#sav-pgz') ? $('#sav-pgz').value : '';
|
||||
const fSport = $('#sav-sport') ? $('#sav-sport').value : '';
|
||||
const fKat = $('#sav-kat') ? $('#sav-kat').value : '';
|
||||
let rows = _cache.savezi || [];
|
||||
if(q) rows = rows.filter(s => (s.naziv||'').toLowerCase().includes(q) || (s.sport||'').toLowerCase().includes(q));
|
||||
if(fSport) rows = rows.filter(s => (s.sport||'')===fSport);
|
||||
if(fKat==='zupanijski') rows = rows.filter(s => /(?:zupanij|županij)/i.test(s.razina||''));
|
||||
else if(fKat==='gradski') rows = rows.filter(s => /gradsk/i.test(s.razina||''));
|
||||
if(pgz==='1') rows = rows.filter(s => s.pgz_relevant);
|
||||
if(_sort.savezi) rows = sortRows(rows, _sort.savezi.key, _sort.savezi.dir);
|
||||
$('#sav-cnt').textContent = rows.length+' saveza';
|
||||
@@ -1256,11 +1275,40 @@ async function openSavez(id){
|
||||
}
|
||||
|
||||
//=========== KLUBOVI ===========
|
||||
async function loadKlubovi(){
|
||||
async
|
||||
// === PGŽ FINANCIRANI FILTER (CRISIS V4) ===
|
||||
window._klubFilters = window._klubFilters || {financiran: false, godisnjak: false};
|
||||
|
||||
window.toggleKlubFilter = function(name){
|
||||
window._klubFilters[name] = !window._klubFilters[name];
|
||||
loadKlubovi();
|
||||
};
|
||||
|
||||
window.renderKlubFilters = function(targetEl){
|
||||
if(!targetEl) return;
|
||||
const f = window._klubFilters;
|
||||
targetEl.innerHTML = `
|
||||
<label style="margin-right:12px;cursor:pointer;color:var(--t1)">
|
||||
<input type="checkbox" ${f.financiran?'checked':''} onchange="toggleKlubFilter('financiran')" data-filter="financiran">
|
||||
💰 Samo financirani od PGŽ
|
||||
</label>
|
||||
<label style="margin-right:12px;cursor:pointer;color:var(--t1)">
|
||||
<input type="checkbox" ${f.godisnjak?'checked':''} onchange="toggleKlubFilter('godisnjak')" data-filter="godisnjak">
|
||||
📖 U godišnjaku
|
||||
</label>
|
||||
<label style="cursor:pointer;color:var(--t1)">
|
||||
<input type="checkbox" ${f.priority_only?'checked':''} onchange="toggleKlubFilter('priority_only')" data-filter="priority_only">
|
||||
⭐ Samo prioritet (financiran ili godišnjak)
|
||||
</label>
|
||||
`;
|
||||
};
|
||||
|
||||
function loadKlubovi(){
|
||||
const root = $('#pg-klubovi');
|
||||
if(!_cache.klubovi){
|
||||
root.innerHTML = '<div class="loading">Učitavanje klubova…</div>';
|
||||
const d = await api('/klubovi?limit=500');
|
||||
// request all clubs sorted by priority (financed-or-godišnjak first) from backend
|
||||
const d = await api('/klubovi?limit=2500');
|
||||
if(!d){ root.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
|
||||
_cache.klubovi = d.rows || [];
|
||||
}
|
||||
@@ -1276,12 +1324,20 @@ function renderKluboviShell(){
|
||||
<input type="search" id="kl-q" placeholder="🔍 Pretraži klub…">
|
||||
<select id="kl-sport"><option value="">Svi sportovi</option>${sports.map(s=>'<option value="'+esc(s)+'">'+esc(s)+'</option>').join('')}</select>
|
||||
<select id="kl-grad"><option value="">Svi gradovi</option>${grads.map(g=>'<option value="'+esc(g)+'">'+esc(g)+'</option>').join('')}</select>
|
||||
<select id="kl-kat" title="Kategorija">
|
||||
<option value="">Sve kategorije</option>
|
||||
<option value="priority">Samo PGŽ priority</option>
|
||||
<option value="financiran">Samo financirani</option>
|
||||
<option value="godisnjak">Samo u godišnjaku</option>
|
||||
</select>
|
||||
<label><input type="checkbox" id="kl-nk"> Nositelj kvalitete</label>
|
||||
<div class="toggle">
|
||||
<button id="kl-card" class="${_state.viewKlubovi==='card'?'active':''}" onclick="setKluboviView('card')">Kartice</button>
|
||||
<button id="kl-table" class="${_state.viewKlubovi==='table'?'active':''}" onclick="setKluboviView('table')">Tablica</button>
|
||||
</div>
|
||||
<button class="btn" style="margin-left:auto" onclick="enrichBulk('klub', 50, 70)">✨ Obogati sve (50)</button>
|
||||
<button class="btn" onclick="exportKlubovi('xlsx')">⬇ XLSX</button>
|
||||
<button class="btn" onclick="exportKlubovi('csv')">⬇ CSV</button>
|
||||
<button class="btn" onclick="enrichBulk('klub', 50, 70)">✨ Obogati (50)</button>
|
||||
<span class="tb-s" id="kl-cnt"></span>
|
||||
</div>
|
||||
<div id="kl-out"></div>
|
||||
@@ -1289,6 +1345,7 @@ function renderKluboviShell(){
|
||||
$('#kl-q').addEventListener('input', debounce(applyKluboviFilter, 200));
|
||||
$('#kl-sport').addEventListener('change', applyKluboviFilter);
|
||||
$('#kl-grad').addEventListener('change', applyKluboviFilter);
|
||||
$('#kl-kat').addEventListener('change', applyKluboviFilter);
|
||||
$('#kl-nk').addEventListener('change', applyKluboviFilter);
|
||||
}
|
||||
function setKluboviView(v){
|
||||
@@ -1301,12 +1358,16 @@ function applyKluboviFilter(){
|
||||
const q = (($('#kl-q')?$('#kl-q').value:'') || '').toLowerCase().trim();
|
||||
const sport = $('#kl-sport') ? $('#kl-sport').value : '';
|
||||
const grad = $('#kl-grad') ? $('#kl-grad').value : '';
|
||||
const kat = $('#kl-kat') ? $('#kl-kat').value : '';
|
||||
const nk = $('#kl-nk') ? $('#kl-nk').checked : false;
|
||||
let rows = _cache.klubovi || [];
|
||||
if(q) rows = rows.filter(k => (k.klub||'').toLowerCase().includes(q) || (k.sport||'').toLowerCase().includes(q));
|
||||
if(sport) rows = rows.filter(k => k.sport===sport);
|
||||
if(grad) rows = rows.filter(k => k.grad===grad);
|
||||
if(nk) rows = rows.filter(k => k.nositelj_kvalitete);
|
||||
if(kat==='priority') rows = rows.filter(k => k.priority);
|
||||
else if(kat==='financiran') rows = rows.filter(k => k.financiran);
|
||||
else if(kat==='godisnjak') rows = rows.filter(k => k.godisnjak);
|
||||
if(_sort.klubovi) rows = sortRows(rows, _sort.klubovi.key, _sort.klubovi.dir);
|
||||
$('#kl-cnt').textContent = rows.length+' klubova';
|
||||
const top = rows.slice(0, 300);
|
||||
@@ -1314,15 +1375,24 @@ function applyKluboviFilter(){
|
||||
if(rows.length>300){
|
||||
$('#kl-out').insertAdjacentHTML('beforeend', '<div class="empty">… i još '+(rows.length-300)+' klubova. Suzite filtre.</div>');
|
||||
}
|
||||
// Wire tickbox-all & individual checks (no-op if absent)
|
||||
const all = $('#kl-all');
|
||||
if(all){
|
||||
all.addEventListener('change', () => {
|
||||
$$('.kl-pick').forEach(cb => { cb.checked = all.checked; });
|
||||
}, {once:true});
|
||||
}
|
||||
}
|
||||
function renderKluboviGrid(rows){
|
||||
if(!rows.length) return '<div class="empty">Nema rezultata</div>';
|
||||
return '<div class="grid-club">'+rows.map(k => `
|
||||
<div class="entity" onclick="openKlub(${k.id})">
|
||||
${k.nositelj_kvalitete?'<div class="et-tag">N.K.</div>':''}
|
||||
${k.priority?'<div class="et-tag" style="background:#ffd700;color:#1a1a1a">★ PRIO</div>':(k.nositelj_kvalitete?'<div class="et-tag">N.K.</div>':'')}
|
||||
<div class="et">${esc(k.klub||k.sport||'(bez naziva)')}</div>
|
||||
<div class="es">${txt(k.razina,'')} · ${txt(k.grad,'—')}</div>
|
||||
<div class="em">
|
||||
${k.financiran?'<span class="tag gd" title="PGŽ sufinanciran">€</span>':''}
|
||||
${k.godisnjak?'<span class="tag b" title="U godišnjaku">G</span>':''}
|
||||
<span><b>${fmtNum(k.registriranih)}</b> reg.</span>
|
||||
<span><b>${fmtNum(k.trenera)}</b> trenera</span>
|
||||
<span><b>${fmtNum(k.reprezentativaca)}</b> repr.</span>
|
||||
@@ -1332,20 +1402,71 @@ function renderKluboviGrid(rows){
|
||||
function renderKluboviTable(rows){
|
||||
if(!rows.length) return '<div class="empty">Nema rezultata</div>';
|
||||
return `<div class="card" style="padding:0;overflow-x:auto"><table>
|
||||
<thead><tr>${sortHeader('klubovi','klub','Klub','')}${sortHeader('klubovi','sport','Sport','')}${sortHeader('klubovi','razina','Razina','')}${sortHeader('klubovi','grad','Grad','')}${sortHeader('klubovi','registriranih','Reg.','num')}${sortHeader('klubovi','trenera','Trenera','num')}${sortHeader('klubovi','nositelj_kvalitete','Status','')}</tr></thead>
|
||||
<thead><tr><th style="width:34px"><input type="checkbox" id="kl-all" title="Označi sve"></th><th title="PGŽ priority">★</th>${sortHeader('klubovi','klub','Klub','')}${sortHeader('klubovi','sport','Sport','')}${sortHeader('klubovi','razina','Razina','')}${sortHeader('klubovi','grad','Grad','')}${sortHeader('klubovi','registriranih','Reg.','num')}${sortHeader('klubovi','trenera','Trenera','num')}${sortHeader('klubovi','nositelj_kvalitete','Status','')}</tr></thead>
|
||||
<tbody>${rows.map(k => `
|
||||
<tr onclick="openKlub(${k.id})">
|
||||
<td><b>${esc(k.klub||k.sport||'(bez naziva)')}</b></td>
|
||||
<td>${txt(k.sport)}</td>
|
||||
<td>${txt(k.razina)}</td>
|
||||
<td>${txt(k.grad)}</td>
|
||||
<td class="num">${fmtNum(k.registriranih)}</td>
|
||||
<td class="num">${fmtNum(k.trenera)}</td>
|
||||
<td>${k.nositelj_kvalitete?'<span class="tag gd">N.K.</span>':''}${k.aktivan?'<span class="tag gr">AKT</span>':'<span class="tag rd">NK</span>'}</td>
|
||||
<tr>
|
||||
<td onclick="event.stopPropagation()"><input type="checkbox" class="kl-pick" data-id="${k.id}"></td>
|
||||
<td onclick="openKlub(${k.id})">${k.priority?'<span class="tag gd" title="financiran ili u godišnjaku">★</span>':''}</td>
|
||||
<td onclick="openKlub(${k.id})"><b>${esc(k.klub||k.sport||'(bez naziva)')}</b></td>
|
||||
<td onclick="openKlub(${k.id})">${txt(k.sport)}</td>
|
||||
<td onclick="openKlub(${k.id})">${txt(k.razina)}</td>
|
||||
<td onclick="openKlub(${k.id})">${txt(k.grad)}</td>
|
||||
<td onclick="openKlub(${k.id})" class="num">${fmtNum(k.registriranih)}</td>
|
||||
<td onclick="openKlub(${k.id})" class="num">${fmtNum(k.trenera)}</td>
|
||||
<td onclick="openKlub(${k.id})">${k.financiran?'<span class="tag gd" title="financiran">€</span>':''}${k.godisnjak?'<span class="tag b" title="godišnjak">G</span>':''}${k.nositelj_kvalitete?'<span class="tag gd">N.K.</span>':''}${k.aktivan?'<span class="tag gr">AKT</span>':'<span class="tag rd">NK</span>'}</td>
|
||||
</tr>`).join('')}</tbody>
|
||||
</table></div>`;
|
||||
}
|
||||
|
||||
// ─── Sport-aware enrichment helper (cached) ───
|
||||
const _enrichSrcCache = {};
|
||||
async function enrichSourceFor(sport){
|
||||
if(!sport) return null;
|
||||
const k = (sport||'').toLowerCase();
|
||||
if(_enrichSrcCache[k]) return _enrichSrcCache[k];
|
||||
try{
|
||||
const d = await api('/v2/enrich-sources?sport='+encodeURIComponent(sport));
|
||||
if(d && d.match){ _enrichSrcCache[k] = d.match; return d.match; }
|
||||
} catch(e){}
|
||||
return null;
|
||||
}
|
||||
async function openEnrichSourceForKlub(sport, naziv){
|
||||
const src = await enrichSourceFor(sport);
|
||||
if(!src){ window.toast && window.toast('Nema definiranog izvora za sport: '+(sport||'?'), 'warn', 3000); return; }
|
||||
const base = (src.base_url||'').replace(/\/$/,'');
|
||||
let url;
|
||||
if((src.sport||'').toLowerCase() === 'nogomet'){
|
||||
url = base + '/klubovi?q=' + encodeURIComponent(naziv||'');
|
||||
} else {
|
||||
url = base + '/?s=' + encodeURIComponent(naziv||'');
|
||||
}
|
||||
window.open(url, '_blank', 'noopener');
|
||||
}
|
||||
|
||||
// ─── Export of selected klubovi ───
|
||||
async function exportKlubovi(format){
|
||||
const ids = $$('.kl-pick').filter(cb => cb.checked).map(cb => parseInt(cb.dataset.id, 10)).filter(Boolean);
|
||||
if(!ids.length){ window.toast && window.toast('Označite najmanje jedan klub (checkbox)', 'warn', 3000); return; }
|
||||
try{
|
||||
const resp = await fetch(API + '/v2/export/klubovi', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ids, format})
|
||||
});
|
||||
if(!resp.ok){ window.toast && window.toast('Export greška: HTTP '+resp.status, 'error', 4000); return; }
|
||||
const blob = await resp.blob();
|
||||
const cd = resp.headers.get('Content-Disposition') || '';
|
||||
const m = cd.match(/filename="?([^"]+)"?/);
|
||||
const fname = m ? m[1] : ('pgz_klubovi.' + format);
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = fname;
|
||||
document.body.appendChild(a); a.click(); a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
|
||||
window.toast && window.toast('Export gotov: '+ids.length+' klubova → '+format.toUpperCase(), 'success', 3000);
|
||||
} catch(e){ window.toast && window.toast('Export greška: '+(e.message||e), 'error', 4000); }
|
||||
}
|
||||
|
||||
async function openKlub(id){
|
||||
openPanel('Klub', '<div class="loading">Učitavanje kluba…</div>');
|
||||
const k = await api('/klubovi/'+id);
|
||||
@@ -1384,7 +1505,9 @@ async function openKlub(id){
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tab active" onclick="switchKlubTab(this,'k-info')">Info</div>
|
||||
<div class="tab" onclick="switchKlubTab(this,'k-clan')">Sportaši (${clanovi.length})</div>
|
||||
<div class="tab" onclick="switchKlubTab(this,'k-clan')">Roster (${clanovi.length})</div>
|
||||
<div class="tab" onclick="switchKlubTab(this,'k-kat')">Kategorije</div>
|
||||
${(k.sport||'').toLowerCase()==='nogomet' ? '<div class="tab" onclick="switchKlubTab(this,\'k-hns\')">HNS karijera</div>' : ''}
|
||||
<div class="tab" onclick="switchKlubTab(this,'k-pot')">Potpore (${potpore.length})</div>
|
||||
</div>
|
||||
|
||||
@@ -1411,24 +1534,64 @@ async function openKlub(id){
|
||||
👥 Vidi sportaše ovog kluba (${clanovi.length})
|
||||
</a>
|
||||
${(k.web||k.web_stranica) ? '<a class="btn" href="'+esc(k.web||k.web_stranica)+'" target="_blank" style="display:inline-flex;align-items:center;gap:6px">🌐 Službena stranica</a>' : ''}
|
||||
<button class="btn" onclick="openEnrichSourceForKlub(${JSON.stringify(k.sport||'')}, ${JSON.stringify(k.naziv||'')})" style="display:inline-flex;align-items:center;gap:6px">🌐 Obogati podatke (sport-savez)</button>
|
||||
</div>
|
||||
${k.napomena ? '<div class="card" style="margin-top:14px"><div class="card-t" style="margin-bottom:6px">Napomena</div><div style="font-size:12px;color:var(--t1);line-height:1.5">'+esc(k.napomena)+'</div></div>' : ''}
|
||||
</div>
|
||||
|
||||
<div id="k-clan" class="ktab" style="display:none">
|
||||
${clanovi.length ? `<div style="overflow-x:auto;max-height:500px;overflow-y:auto"><table>
|
||||
<thead><tr><th>Sportaš</th><th>Spol</th><th>Pozicija</th><th>Tagovi</th></tr></thead>
|
||||
<thead><tr><th>Sportaš</th><th>Spol</th><th>Pozicija</th><th>Kategorija</th><th>Tagovi</th></tr></thead>
|
||||
<tbody>${clanovi.map(c => `
|
||||
<tr onclick="closePanel();setTimeout(()=>openSportas(${c.id}),250)">
|
||||
<td><b>${esc(c.ime||'')} ${esc(c.prezime||'')}</b></td>
|
||||
<td>${txt(c.spol)}</td>
|
||||
<td>${txt(c.pozicija)}</td>
|
||||
<td>${txt(c.kategorija)}</td>
|
||||
<td>${c.reprezentativac?'<span class="tag gd">REPR</span>':''}${c.kategoriziran?'<span class="tag b">KAT</span>':''}${c.stipendiran?'<span class="tag gr">STIP</span>':''}</td>
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table></div>` : '<div class="empty">Nema podataka o sportašima</div>'}
|
||||
</div>
|
||||
|
||||
<div id="k-kat" class="ktab" style="display:none">
|
||||
${(() => {
|
||||
if(!clanovi.length) return '<div class="empty">Nema podataka za grupiranje</div>';
|
||||
const groups = {};
|
||||
clanovi.forEach(c => {
|
||||
const cats = (c.kategorije && c.kategorije.length) ? c.kategorije : [c.kategorija || '(nepoznata)'];
|
||||
cats.forEach(kat => {
|
||||
const key = kat || '(nepoznata)';
|
||||
(groups[key] = groups[key] || []).push(c);
|
||||
});
|
||||
});
|
||||
return Object.keys(groups).sort().map(kat => `
|
||||
<details style="margin-bottom:8px" ${groups[kat].length<=12?'open':''}>
|
||||
<summary style="cursor:pointer;padding:8px;background:rgba(255,255,255,.04);border-radius:6px"><b>${esc(kat)}</b> · ${groups[kat].length} igrač${groups[kat].length===1?'':'a'}</summary>
|
||||
<table style="margin-top:6px"><tbody>${groups[kat].map(c => `
|
||||
<tr onclick="closePanel();setTimeout(()=>openSportas(${c.id}),250)">
|
||||
<td><b>${esc(c.ime||'')} ${esc(c.prezime||'')}</b></td>
|
||||
<td>${txt(c.spol)}</td>
|
||||
<td>${txt(c.pozicija)}</td>
|
||||
</tr>`).join('')}</tbody></table>
|
||||
</details>`).join('');
|
||||
})()}
|
||||
</div>
|
||||
|
||||
${(k.sport||'').toLowerCase()==='nogomet' ? `
|
||||
<div id="k-hns" class="ktab" style="display:none">
|
||||
<div class="card" style="padding:14px">
|
||||
<div class="card-t" style="margin-bottom:8px">⚽ HNS Semafor karijera</div>
|
||||
<div style="font-size:13px;color:var(--t1);line-height:1.6">
|
||||
Klikni na pojedinog igrača (tab "Roster") za detalje sezona, golova i utakmica iz HNS Semafora.
|
||||
${(k.naziv && k.naziv.match(/HNK|NK |GNK|RNK/i)) ? '<br><br><b>Tip:</b> Ovaj klub vjerojatno postoji na HNS Semaforu.' : ''}
|
||||
</div>
|
||||
<div style="margin-top:14px">
|
||||
<button class="btn" onclick="openEnrichSourceForKlub('nogomet', ${JSON.stringify(k.naziv||'')})">🌐 Otvori HNS Semafor pretragu</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>` : ''}
|
||||
|
||||
<div id="k-pot" class="ktab" style="display:none">
|
||||
${potpore.length ? `<div style="overflow-x:auto"><table>
|
||||
<thead><tr><th>Godina</th><th>Naziv</th><th class="num">Iznos</th></tr></thead>
|
||||
@@ -1455,6 +1618,61 @@ function switchKlubTab(el, tabId){
|
||||
if(target) target.style.display='block';
|
||||
}
|
||||
|
||||
//=========== IGRAČI PO KATEGORIJI ===========
|
||||
async function loadIgraciKat(){
|
||||
const root = $('#pg-igraci-kat');
|
||||
root.innerHTML = `
|
||||
<div class="toolbar">
|
||||
<select id="ik-sport"><option value="">Svi sportovi</option></select>
|
||||
<select id="ik-klub"><option value="">Svi klubovi</option></select>
|
||||
<button class="btn primary" onclick="reloadIgraciKat()">↻ Osvježi</button>
|
||||
<span class="tb-s" id="ik-cnt"></span>
|
||||
</div>
|
||||
<div id="ik-out"><div class="loading">Učitavanje…</div></div>
|
||||
`;
|
||||
// Populate sport dropdown from already-cached klubovi if present
|
||||
const sports = Array.from(new Set((_cache.klubovi||[]).map(k=>k.sport).filter(Boolean))).sort();
|
||||
const ss = $('#ik-sport');
|
||||
sports.forEach(s => { const o=document.createElement('option'); o.value=s; o.textContent=s; ss.appendChild(o); });
|
||||
const klubovi = (_cache.klubovi||[]).map(k => [k.id, k.klub||k.naziv]).sort((a,b)=>String(a[1]).localeCompare(String(b[1]),'hr'));
|
||||
const sk = $('#ik-klub');
|
||||
klubovi.slice(0,400).forEach(([id,n]) => { const o=document.createElement('option'); o.value=id; o.textContent=n; sk.appendChild(o); });
|
||||
$('#ik-sport').addEventListener('change', reloadIgraciKat);
|
||||
$('#ik-klub').addEventListener('change', reloadIgraciKat);
|
||||
reloadIgraciKat();
|
||||
}
|
||||
async function reloadIgraciKat(){
|
||||
const sport = $('#ik-sport') ? $('#ik-sport').value : '';
|
||||
const klub = $('#ik-klub') ? $('#ik-klub').value : '';
|
||||
const out = $('#ik-out');
|
||||
out.innerHTML = '<div class="loading">Učitavanje…</div>';
|
||||
let qs = '?limit_per_kat=200';
|
||||
if(sport) qs += '&sport=' + encodeURIComponent(sport);
|
||||
if(klub) qs += '&klub_id=' + encodeURIComponent(klub);
|
||||
const d = await api('/v2/sportasi-by-kategorija' + qs);
|
||||
if(!d || !d.groups){ out.innerHTML = '<div class="empty">Greška pri dohvatu</div>'; return; }
|
||||
$('#ik-cnt').textContent = d.total_kategorija + ' kategorija';
|
||||
if(!d.groups.length){ out.innerHTML = '<div class="empty">Nema rezultata</div>'; return; }
|
||||
out.innerHTML = d.groups.map(g => `
|
||||
<details style="margin-bottom:10px" ${g.count<=12?'open':''}>
|
||||
<summary style="cursor:pointer;padding:10px;background:rgba(255,255,255,.04);border-radius:8px;font-size:14px"><b>${esc(g.kategorija)}</b> · ${g.count} igrač${g.count===1?'':'a'}</summary>
|
||||
<div class="card" style="padding:0;margin-top:6px;overflow-x:auto"><table>
|
||||
<thead><tr><th>Igrač</th><th>Klub</th><th>Sport</th><th>Pozicija</th><th>Godina rođ.</th><th>Tagovi</th></tr></thead>
|
||||
<tbody>${(g.rows||[]).map(c => `
|
||||
<tr onclick="openSportas(${c.id})">
|
||||
<td><b>${esc((c.ime||'')+' '+(c.prezime||''))}</b></td>
|
||||
<td>${txt(c.klub_naziv)}</td>
|
||||
<td>${txt(c.sport)}</td>
|
||||
<td>${txt(c.pozicija)}</td>
|
||||
<td>${c.datum_rodenja ? String(c.datum_rodenja).slice(0,4) : '—'}</td>
|
||||
<td>${c.reprezentativac?'<span class="tag gd">REPR</span>':''}${c.kategoriziran?'<span class="tag b">KAT</span>':''}${c.stipendiran?'<span class="tag gr">STIP</span>':''}</td>
|
||||
</tr>`).join('')}</tbody>
|
||||
</table></div>
|
||||
</details>
|
||||
`).join('');
|
||||
}
|
||||
function applyIgraciKatFilter(){ /* hook for setSort; igraci-kat re-renders via reloadIgraciKat */ }
|
||||
|
||||
//=========== SPORTAŠI ===========
|
||||
async function loadSportasi(){
|
||||
const root = $('#pg-sportasi');
|
||||
@@ -1469,9 +1687,23 @@ async function loadSportasi(){
|
||||
}
|
||||
function renderSportasiShell(){
|
||||
const root = $('#pg-sportasi');
|
||||
const sports = Array.from(new Set((_cache.clanovi||[]).map(c=>c.sport).filter(Boolean))).sort();
|
||||
const klubovi = Array.from(new Map((_cache.clanovi||[]).filter(c=>c.klub_id).map(c=>[c.klub_id, c.klub_naziv_godisnjak||('Klub #'+c.klub_id)])).entries()).sort((a,b)=>String(a[1]).localeCompare(String(b[1]),'hr'));
|
||||
const kats = Array.from(new Set((_cache.clanovi||[]).flatMap(c => (c.kategorije && c.kategorije.length ? c.kategorije : [c.kategorija]).filter(Boolean)))).sort();
|
||||
root.innerHTML = `
|
||||
<div class="toolbar">
|
||||
<input type="search" id="sp-q" placeholder="🔍 Ime ili prezime…">
|
||||
<select id="sp-sport"><option value="">Svi sportovi</option>${sports.map(s=>'<option value="'+esc(s)+'">'+esc(s)+'</option>').join('')}</select>
|
||||
<select id="sp-klub"><option value="">Svi klubovi</option>${klubovi.slice(0,400).map(([id,n])=>'<option value="'+id+'">'+esc(n)+'</option>').join('')}</select>
|
||||
<select id="sp-kat"><option value="">Sve kategorije</option>${kats.map(k=>'<option value="'+esc(k)+'">'+esc(k)+'</option>').join('')}</select>
|
||||
<input type="number" id="sp-god" placeholder="Godina rođ." min="1900" max="2030" style="width:120px">
|
||||
<select id="sp-status">
|
||||
<option value="">Svi statusi</option>
|
||||
<option value="aktivan">Aktivni</option>
|
||||
<option value="reprezentativac">Reprezentativci</option>
|
||||
<option value="kategoriziran">Kategorizirani</option>
|
||||
<option value="stipendiran">Stipendirani</option>
|
||||
</select>
|
||||
<select id="sp-hoo">
|
||||
<option value="">Sve HOO kategorije</option>
|
||||
<option value="1">I. kategorija</option>
|
||||
@@ -1493,6 +1725,11 @@ function renderSportasiShell(){
|
||||
<div id="sp-out"></div>
|
||||
`;
|
||||
$('#sp-q').addEventListener('input', debounce(applySportasiFilter, 200));
|
||||
$('#sp-sport').addEventListener('change', applySportasiFilter);
|
||||
$('#sp-klub').addEventListener('change', applySportasiFilter);
|
||||
$('#sp-kat').addEventListener('change', applySportasiFilter);
|
||||
$('#sp-god').addEventListener('input', debounce(applySportasiFilter, 250));
|
||||
$('#sp-status').addEventListener('change', applySportasiFilter);
|
||||
$('#sp-hoo').addEventListener('change', applySportasiFilter);
|
||||
$('#sp-rep').addEventListener('change', applySportasiFilter);
|
||||
$('#sp-foto').addEventListener('change', applySportasiFilter);
|
||||
@@ -1574,8 +1811,21 @@ function applySportasiFilter(){
|
||||
const hoo = $('#sp-hoo') ? $('#sp-hoo').value : '';
|
||||
const rep = $('#sp-rep') ? $('#sp-rep').checked : false;
|
||||
const foto = $('#sp-foto') ? $('#sp-foto').checked : false;
|
||||
const fSport = $('#sp-sport') ? $('#sp-sport').value : '';
|
||||
const fKlub = $('#sp-klub') ? $('#sp-klub').value : '';
|
||||
const fKat = $('#sp-kat') ? $('#sp-kat').value : '';
|
||||
const fGod = $('#sp-god') ? $('#sp-god').value.trim() : '';
|
||||
const fStat = $('#sp-status') ? $('#sp-status').value : '';
|
||||
let rows = _cache.clanovi || [];
|
||||
if(q) rows = rows.filter(c => ((c.ime||'')+' '+(c.prezime||'')).toLowerCase().includes(q));
|
||||
if(fSport) rows = rows.filter(c => (c.sport||'') === fSport);
|
||||
if(fKlub) rows = rows.filter(c => String(c.klub_id||'') === String(fKlub));
|
||||
if(fKat) rows = rows.filter(c => (c.kategorije && c.kategorije.includes(fKat)) || c.kategorija === fKat);
|
||||
if(fGod) rows = rows.filter(c => String(c.datum_rodenja||c.datum_rodjenja||'').slice(0,4) === fGod);
|
||||
if(fStat === 'aktivan') rows = rows.filter(c => c.aktivan);
|
||||
else if(fStat === 'reprezentativac') rows = rows.filter(c => c.reprezentativac);
|
||||
else if(fStat === 'kategoriziran') rows = rows.filter(c => c.kategoriziran);
|
||||
else if(fStat === 'stipendiran') rows = rows.filter(c => c.stipendiran);
|
||||
if(rep) rows = rows.filter(c => c.reprezentativac);
|
||||
if(foto) rows = rows.filter(c => c.slika_url);
|
||||
if(hoo) rows = rows.filter(c => String(c.hoo_kategorija||c.kategorija_hoo||'')===hoo);
|
||||
|
||||
Reference in New Issue
Block a user