Files
pgz-sport/static/sport2.html
T
claude-cc1 98f823b4d9 CC1 R3B-P4 — Forenzika scan radi
Backend:
- enrich_router.py: POST /api/v2/forensic/scan {name} → searches civic.persons,
  joins person_entity_links, scans forensic_findings (by OIB + by name),
  synthesises per-person risk score (PEP function +30, party +15, links +5×, findings +10×, crit +20).
- Forced PG_HOST to 10.10.0.2 when env says localhost (local PG disabled).

Frontend:
- New scan card with name input + "Pokreni" button on Forenzika section.
- Renders matched persons with risk score, links, findings.
- Test: "Velimir Liverić" → 2 osoba, 2 linka, OIB 91528083847 found.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 00:10:21 +02:00

2396 lines
120 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="hr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>PGŽ SPORT — Platforma</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<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">
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
<script src="https://unpkg.com/three@0.160.0/build/three.min.js"></script>
<script src="https://unpkg.com/3d-force-graph@1.73.0/dist/3d-force-graph.min.js"></script>
<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}
a{color:var(--cyan);text-decoration:none}
a:hover{color:var(--pgz-gold)}
button,input,select{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}
::-webkit-scrollbar-thumb:hover{background:var(--pgz-blue2)}
.app{display:flex;min-height:100vh}
.sb{width:240px;background:linear-gradient(180deg,var(--bg1) 0%,var(--bg0) 100%);border-right:1px solid var(--rim);position:fixed;top:0;left:0;bottom:0;display:flex;flex-direction:column;z-index:10;transition:width .22s ease}
.sb-h{padding:18px 18px 14px;border-bottom:1px solid var(--rim);position:relative}
.sb-h .logo{font-weight:800;font-size:14px;color:var(--t0);letter-spacing:.5px;white-space:nowrap;overflow:hidden}
.sb-h .logo .g{color:var(--pgz-gold)}
.sb-h .sub{font-size:10px;color:var(--t2);margin-top:4px;text-transform:uppercase;letter-spacing:1px;white-space:nowrap;overflow:hidden}
.sb-toggle{position:absolute;top:14px;right:8px;width:22px;height:22px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--t2);background:var(--bg2);border:1px solid var(--rim);border-radius:4px;font-size:11px;font-weight:700;transition:all .15s;user-select:none}
.sb-toggle:hover{background:var(--bg3);color:var(--pgz-gold);border-color:var(--pgz-gold)}
.sb-nav{flex:1;padding:10px 8px;overflow-y:auto;overflow-x:hidden}
.nav-i{padding:9px 12px;border-radius:6px;color:var(--t2);cursor:pointer;display:flex;align-items:center;gap:10px;font-size:12.5px;margin-bottom:2px;transition:background .15s,color .15s;white-space:nowrap}
.nav-i:hover{background:var(--bg2);color:var(--t1)}
.nav-i.active{background:linear-gradient(90deg,var(--pgz-blue) 0%,var(--pgz-blue2) 100%);color:#fff;font-weight:600}
.nav-i .ic{width:18px;text-align:center;font-size:14px;flex-shrink:0}
.nav-i .lbl{overflow:hidden;text-overflow:ellipsis}
.sb-foot{padding:10px 14px;border-top:1px solid var(--rim);font-size:10px;color:var(--t4);white-space:nowrap;overflow:hidden}
/* Collapsed sidebar */
.sb.collapsed{width:58px}
.sb.collapsed .sb-h{padding:18px 8px 14px;text-align:center}
.sb.collapsed .sb-h .logo{font-size:0}
.sb.collapsed .sb-h .logo::before{content:"PG";font-size:13px;color:var(--pgz-gold);font-weight:800}
.sb.collapsed .sb-h .sub{display:none}
.sb.collapsed .sb-toggle{position:static;margin:6px auto 0;display:flex}
.sb.collapsed .nav-i{justify-content:center;padding:10px 6px}
.sb.collapsed .nav-i .lbl{display:none}
.sb.collapsed .nav-i{position:relative}
.sb.collapsed .nav-i:hover::after{content:attr(data-label);position:absolute;left:58px;top:50%;transform:translateY(-50%);background:var(--bg3);color:var(--t0);padding:5px 10px;border-radius:4px;font-size:11.5px;white-space:nowrap;border:1px solid var(--rim);z-index:50;font-weight:600;pointer-events:none;box-shadow:2px 2px 8px rgba(0,0,0,.4)}
.sb.collapsed .sb-foot{font-size:0;padding:8px}
.sb.collapsed .sb-foot::before{content:"v2";font-size:9px;color:var(--t4)}
.main{margin-left:240px;flex:1;min-width:0;transition:margin-left .22s ease}
.sb.collapsed ~ .main{margin-left:58px}
.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:22px}
.section{display:none}
.section.active{display:block}
.kpi-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px;margin-bottom:22px}
.kpi{background:linear-gradient(135deg,var(--bg2) 0%,var(--bg1) 100%);border:1px solid var(--rim);border-radius:8px;padding:14px 16px;position:relative;overflow:hidden}
.kpi::before{content:"";position:absolute;top:0;left:0;width:3px;height:100%;background:var(--pgz-gold)}
.kpi.b::before{background:var(--pgz-blue2)}
.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:24px;font-weight:800;color:var(--t0);margin-top:4px;font-family:var(--mono)}
.kpi-s{font-size:10px;color:var(--t4);margin-top:2px}
.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}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px}
.grid-club{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}
.grid-player{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px}
.entity{background:var(--bg2);border:1px solid var(--rim);border-radius:8px;padding:12px;cursor:pointer;transition:all .2s;position:relative}
.entity:hover{border-color:var(--pgz-gold);background:var(--bg3);transform:translateY(-1px)}
.entity .et{font-weight:700;color:var(--t0);font-size:13px;margin-bottom:6px;line-height:1.3}
.entity .es{font-size:11px;color:var(--t2);margin-bottom:6px}
.entity .em{display:flex;gap:8px;font-size:10.5px;color:var(--t4);flex-wrap:wrap}
.entity .em b{color:var(--pgz-gold);font-weight:700}
.entity .et-tag{position:absolute;top:8px;right:8px;font-size:9px;padding:2px 6px;border-radius:3px;background:var(--pgz-blue);color:#fff;font-weight:600;text-transform:uppercase}
.player-card{background:var(--bg2);border:1px solid var(--rim);border-radius:8px;overflow:hidden;cursor:pointer;transition:all .2s}
.player-card:hover{border-color:var(--pgz-gold);transform:translateY(-1px)}
.player-card .ph{height:120px;background:linear-gradient(135deg,var(--bg3),var(--bg2));position:relative;overflow:hidden;display:flex;align-items:center;justify-content:center}
.player-card .ph img{width:100%;height:100%;object-fit:cover;object-position:top}
.player-card .ph .no{font-size:36px;color:var(--t4);font-weight:800}
.player-card .pb{padding:10px}
.player-card .pn{font-weight:700;color:var(--t0);font-size:13px;line-height:1.2}
.player-card .pp{font-size:11px;color:var(--t2);margin-top:3px}
.player-card .pk{font-size:10.5px;color:var(--t4);margin-top:3px}
.player-card .badges{display:flex;gap:4px;margin-top:6px;flex-wrap:wrap}
.player-card .badge{font-size:9px;padding:2px 5px;border-radius:3px;background:var(--bg4);color:var(--t1);text-transform:uppercase;font-weight:600}
.player-card .badge.repr{background:var(--pgz-gold);color:var(--bg0)}
.player-card .badge.hoo{background:var(--pgz-blue2);color:#fff}
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:8px 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)}
table tbody tr.no-click{cursor:default}
table tbody tr.no-click:hover{background:transparent}
.num{font-family:var(--mono);text-align:right}
.toolbar{display:flex;align-items:center;gap:10px;margin-bottom:14px;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 input{min-width:200px}
.toolbar label{font-size:11px;color:var(--t2);display:flex;align-items:center;gap:6px}
.toolbar input[type=checkbox]{accent-color:var(--pgz-gold)}
.btn{background:var(--bg2);border:1px solid var(--rim);border-radius:5px;padding:7px 12px;color:var(--t1);font-size:12px;cursor:pointer;font-weight:600;transition:all .15s}
.btn:hover{background:var(--bg3);border-color:var(--rim2)}
.btn.primary{background:linear-gradient(135deg,var(--pgz-blue),var(--pgz-blue2));border-color:transparent;color:#fff}
.btn.primary:hover{filter:brightness(1.1)}
.toggle{display:inline-flex;background:var(--bg2);border:1px solid var(--rim);border-radius:5px;overflow:hidden}
.toggle button{background:transparent;border:0;padding:6px 12px;color:var(--t2);font-size:11px;font-weight:600;cursor:pointer}
.toggle button.active{background:var(--pgz-blue);color:#fff}
.tabs{display:flex;gap:4px;border-bottom:1px solid var(--rim);margin-bottom:14px;flex-wrap:wrap}
.tab{padding:8px 14px;cursor:pointer;color:var(--t2);font-weight:600;font-size:12px;border-bottom:2px solid transparent;transition:all .15s}
.tab:hover{color:var(--t1)}
.tab.active{color:var(--pgz-gold);border-color:var(--pgz-gold)}
.empty{text-align:center;padding:30px;color:var(--t4);font-size:12px;font-style:italic}
.loading{padding:24px;text-align:center;color:var(--t2);font-size:12px}
.loading::before{content:"";display:inline-block;width:12px;height:12px;border:2px solid var(--rim);border-top-color:var(--pgz-gold);border-radius:50%;animation:spin .8s linear infinite;margin-right:8px;vertical-align:middle}
@keyframes spin{to{transform:rotate(360deg)}}
.tag{display:inline-block;padding:2px 7px;font-size:10px;border-radius:3px;background:var(--bg4);color:var(--t1);font-weight:600;text-transform:uppercase;letter-spacing:.5px;margin-right:3px}
a.tag,.tag[onclick]{cursor:pointer;text-decoration:none;transition:transform .12s,filter .12s}
a.tag:hover,.tag[onclick]:hover{transform:translateY(-1px);filter:brightness(1.15)}
.link-chip{color:var(--cyan);cursor:pointer;text-decoration:none;border-bottom:1px dashed transparent;transition:all .15s}
.link-chip:hover{color:var(--pgz-gold);border-bottom-color:var(--pgz-gold)}
.kv .v a{color:var(--cyan)}
.kv .v a:hover{color:var(--pgz-gold)}
.tag.b{background:var(--pgz-blue);color:#fff}
.tag.gd{background:var(--pgz-gold);color:var(--bg0)}
.tag.gr{background:var(--green);color:var(--bg0)}
.tag.rd{background:var(--red);color:#fff}
.tag.am{background:var(--amber);color:var(--bg0)}
#panel{position:fixed;top:0;right:-620px;width:600px;max-width:96vw;height:100vh;background:var(--bg1);border-left:1px solid var(--rim);z-index:200;transition:right .25s ease;display:flex;flex-direction:column;box-shadow:-8px 0 30px rgba(0,0,0,.5)}
#panel.open{right:0}
#panel-hdr{padding:14px 16px;border-bottom:1px solid var(--rim);display:flex;align-items:center;justify-content:space-between;flex-shrink:0;background:var(--bg2)}
#panel-hdr-t{font-size:14px;font-weight:700;color:var(--t0)}
#panel-x{cursor:pointer;font-size:22px;color:var(--t4);width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:5px;transition:all .15s}
#panel-x:hover{background:var(--bg3);color:var(--red)}
#panel-body{flex:1;overflow-y:auto;padding:16px}
#panel-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:199;backdrop-filter:blur(2px)}
#panel-overlay.open{display:block}
.pp-hdr{display:flex;gap:14px;align-items:flex-start;margin-bottom:14px;padding-bottom:14px;border-bottom:1px solid var(--rim)}
.pp-foto{width:90px;height:110px;border-radius:6px;background:var(--bg3);overflow:hidden;flex-shrink:0;display:flex;align-items:center;justify-content:center}
.pp-foto img{width:100%;height:100%;object-fit:cover;object-position:top}
.pp-foto .no{font-size:32px;color:var(--t4);font-weight:800}
.pp-info{flex:1;min-width:0}
.pp-name{font-size:18px;font-weight:800;color:var(--t0);margin-bottom:4px}
.pp-meta{font-size:12px;color:var(--t2);margin-bottom:6px}
.pp-tags{display:flex;gap:4px;flex-wrap:wrap}
.pp-stats{display:grid;grid-template-columns:repeat(6,1fr);gap:8px;margin-bottom:14px;padding:12px;background:var(--bg2);border:1px solid var(--rim);border-radius:6px}
.pp-stat{text-align:center}
.pp-stat .v{font-size:20px;font-weight:800;color:var(--pgz-gold);font-family:var(--mono)}
.pp-stat .l{font-size:9px;color:var(--t4);text-transform:uppercase;letter-spacing:.5px;margin-top:2px;font-weight:600}
.kv{display:grid;grid-template-columns:140px 1fr;gap:6px 12px;font-size:12px}
.kv .k{color:var(--t2);font-weight:600}
.kv .v{color:var(--t1);word-break:break-word}
.utlogo{width:22px;height:22px;border-radius:50%;background:var(--bg3);object-fit:contain;vertical-align:middle;margin-right:6px}
.score{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:4px;background:var(--bg3);font-size:11px;font-weight:600}
.score.high{background:rgba(0,232,143,.15);color:var(--green)}
.score.mid{background:rgba(245,158,11,.15);color:var(--amber)}
.score.low{background:rgba(255,45,85,.15);color:var(--red)}
.row-2{display:grid;grid-template-columns:1fr 1fr;gap:14px}
@media (max-width:900px){.row-2{grid-template-columns:1fr}}
.chart-box{background:var(--bg2);border:1px solid var(--rim);border-radius:8px;padding:14px;height:340px}
.chart-box canvas{max-height:300px}
.alert-card{padding:10px 12px;border-left:3px solid var(--amber);background:var(--bg2);border-radius:5px;margin-bottom:8px}
.alert-card.crit{border-color:var(--red)}
.alert-card .at{font-weight:700;font-size:12px;color:var(--t0)}
.alert-card .ad{font-size:11px;color:var(--t2);margin-top:3px}
.iframe-map{width:100%;height:140px;border:0;border-radius:5px;background:var(--bg3)}
@media (max-width:768px){
.sb{transform:translateX(-100%);transition:transform .25s}
.sb.open{transform:translateX(0)}
.main{margin-left:0}
.pp-stats{grid-template-columns:repeat(3,1fr)}
}
</style>
</head>
<body>
<div class="app">
<aside class="sb" id="sb">
<div class="sb-h">
<div class="logo">PGŽ <span class="g">SPORT</span></div>
<div class="sub">Primorsko-goranska županija</div>
<div class="sb-toggle" id="sb-toggle" onclick="toggleSidebar()" title="Skupi/raširi sidebar"></div>
</div>
<nav class="sb-nav" id="nav"></nav>
<div class="sb-foot">v2.0 · 2026</div>
</aside>
<main class="main">
<div class="tb">
<div>
<div class="tb-t" id="tb-t">Dashboard</div>
<div class="tb-s" id="tb-s">Pregled stanja</div>
</div>
<div class="tb-s">
<span style="color:var(--green)"></span> API live · sport.rinet.one
</div>
</div>
<div class="content">
<section id="pg-dashboard" class="section active"></section>
<section id="pg-savezi" class="section"></section>
<section id="pg-klubovi" class="section"></section>
<section id="pg-sportasi" class="section"></section>
<section id="pg-financije" class="section"></section>
<section id="pg-objekti" class="section"></section>
<section id="pg-manifestacije" class="section"></section>
<section id="pg-mreza" class="section"></section>
<section id="pg-forenzika" class="section"></section>
</div>
</main>
</div>
<div id="panel-overlay" onclick="closePanel()"></div>
<div id="panel">
<div id="panel-hdr">
<div id="panel-hdr-t">Detalji</div>
<div id="panel-x" onclick="closePanel()">×</div>
</div>
<div id="panel-body"></div>
</div>
<script>
//=========== CONFIG ===========
const API = '/sport/api';
const NAV_ITEMS = [
{id:'dashboard', ic:'\u{1F4CA}', label:'Dashboard'},
{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:'objekti', ic:'\u{1F4CD}', label:'Objekti'},
{id:'manifestacije', ic:'\u{1F4C5}', label:'Manifestacije'},
{id:'mreza', ic:'\u{1F578}', label:'Mreža'},
{id:'forenzika', ic:'⚠', label:'Forenzika'}
];
const SECTION_TITLES = {
dashboard: ['Dashboard', 'Pregled stanja PGŽ Sporta'],
savezi: ['Savezi', '246 sportskih saveza'],
klubovi: ['Klubovi', 'Sportski klubovi PGŽ'],
sportasi: ['Sportaši', 'Registrirani članovi'],
financije: ['Financije', 'Sufinanciranje sporta'],
objekti: ['Sportski objekti', 'Geocodirana infrastruktura'],
manifestacije: ['Manifestacije', 'Sportski događaji'],
mreza: ['Mreža', 'Force-directed graf entiteta i veza'],
forenzika: ['Forenzika', 'Kritični nalazi i alarmi']
};
const _cache = {savezi:null, klubovi:null, clanovi:null, objekti:null, manifestacije:null, sufin:null, dash:null};
const _state = {section:'dashboard', viewSavezi:'card', viewKlubovi:'card', viewSportasi:'card', viewObjekti:'card', viewManif:'card', viewFinancije:'table'};
const _sort = {savezi:null, klubovi:null, sportasi:null, objekti:null, manifestacije:null, financije:null};
let _proracunChart=null, _financijeChart=null;
//=========== UTIL ===========
const $ = s => document.querySelector(s);
const $$ = s => Array.from(document.querySelectorAll(s));
function esc(s){
if(s===null||s===undefined) return '';
return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function fmtNum(n){
if(n===null||n===undefined||n==='') return '—';
const v = Number(n);
if(isNaN(v)) return esc(n);
return v.toLocaleString('hr-HR');
}
function fmtEur(n){
if(n===null||n===undefined||n==='') return '—';
const v = Number(n);
if(isNaN(v)) return esc(n);
if(v>=1000000) return '€'+(v/1000000).toFixed(2)+'M';
if(v>=1000) return '€'+(v/1000).toFixed(1)+'k';
return '€'+v.toFixed(0);
}
function fmtEurFull(n){
if(n===null||n===undefined||n==='') return '—';
const v = Number(n);
if(isNaN(v)) return esc(n);
return v.toLocaleString('hr-HR', {minimumFractionDigits:2, maximumFractionDigits:2})+' €';
}
function fmtDate(s){
if(!s) return '—';
const m = String(s).match(/^(\d{4})-(\d{2})-(\d{2})/);
if(!m) return esc(s);
return m[3]+'.'+m[2]+'.'+m[1];
}
function txt(v, fb){
if(v===null||v===undefined||v==='') return fb===undefined?'—':fb;
return esc(v);
}
async function api(path){
try{
const r = await fetch(API+path);
if(!r.ok) throw new Error('HTTP '+r.status);
return await r.json();
}catch(e){
console.error('API error', path, e);
return null;
}
}
async function apiPost(path, body){
try{
const r = await fetch(API+path, {method:'POST', headers:{'Content-Type':'application/json'}, body: body?JSON.stringify(body):'{}'});
if(!r.ok) throw new Error('HTTP '+r.status);
return await r.json();
}catch(e){
console.error('API POST error', path, e);
return null;
}
}
async function enrichEntity(kind, id){
const targetId = 'enrich-out-'+kind+'-'+id;
const target = document.getElementById(targetId);
if(target) target.innerHTML = '<div class="loading">⏳ Obogaćivanje u tijeku — pretraživanje izvora…</div>';
const r = await apiPost('/v2/enrich/'+kind+'/'+id);
if(!r){ if(target) target.innerHTML = '<div class="empty">Greška pri obogaćivanju</div>'; return; }
const cov = r.coverage||0;
const covCls = cov>=70?'high':(cov>=40?'mid':'low');
const html = `
<div style="display:flex;gap:8px;align-items:center;margin-bottom:10px">
<span class="tag gr">🟢 OBOGAĆENO</span>
<span class="score ${covCls}">Coverage ${cov}%</span>
<span class="tb-s">${r.filled_fields}/${r.total_fields} polja popunjeno</span>
</div>
${r.live_snippet && r.live_snippet.title ? `
<div style="padding:10px;background:var(--bg3);border-left:3px solid var(--pgz-gold);border-radius:5px;margin-bottom:10px">
<div style="font-size:11px;color:var(--t4);text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px">📡 Live snippet</div>
<div style="font-weight:700;color:var(--t0);font-size:13px;margin-bottom:4px">${esc(r.live_snippet.title)}</div>
${r.live_snippet.description ? '<div style="font-size:11.5px;color:var(--t1);line-height:1.5">'+esc(r.live_snippet.description)+'</div>' : ''}
<div style="margin-top:6px"><a href="${esc(r.live_snippet.url)}" target="_blank">↗ ${esc(r.live_snippet.url.slice(0,80))}</a></div>
</div>
` : ''}
${r.missing_fields && r.missing_fields.length ? `
<div style="margin-bottom:10px">
<div style="font-size:11px;color:var(--t2);margin-bottom:4px">Nedostaje:</div>
<div>${r.missing_fields.map(f=>'<span class="tag rd">'+esc(f)+'</span>').join('')}</div>
</div>
` : ''}
<div>
<div style="font-size:11px;color:var(--t2);margin-bottom:6px">🔍 Istraži dalje:</div>
<div style="display:flex;flex-wrap:wrap;gap:6px">
${(r.research_links||[]).map(l => '<a href="'+esc(l.url)+'" target="_blank" class="btn">'+l.icon+' '+esc(l.label)+'</a>').join('')}
</div>
</div>
`;
if(target) target.innerHTML = html;
}
function enrichBlock(kind, id){
return `
<div class="card" id="enrich-card-${kind}-${id}">
<div class="card-h">
<div class="card-t">✨ Obogati podatke</div>
<button class="btn primary" onclick="enrichEntity('${kind}',${id})">▶ Pokreni</button>
</div>
<div id="enrich-out-${kind}-${id}">
<div class="empty" style="padding:14px">Klikom na "Pokreni" platforma će pretražiti vanjske izvore (Google, Wikipedia, službene web stranice) i prikazati dopune za ovu entitetsku karticu.</div>
</div>
</div>
`;
}
function sortRows(rows, key, dir){
if(!key) return rows;
const sorted = rows.slice();
sorted.sort((a,b) => {
let av = a[key], bv = b[key];
if(av==null && bv==null) return 0;
if(av==null) return 1;
if(bv==null) return -1;
const an = typeof av === 'number' ? av : Number(av);
const bn = typeof bv === 'number' ? bv : Number(bv);
if(!isNaN(an) && !isNaN(bn) && (typeof av === 'number' || /^[-+]?\d+(\.\d+)?$/.test(String(av)))){
return dir==='asc' ? (an-bn) : (bn-an);
}
const cmp = String(av).localeCompare(String(bv), 'hr', {numeric:true});
return dir==='asc' ? cmp : -cmp;
});
return sorted;
}
function setSort(section, key){
const cur = _sort[section];
if(cur && cur.key === key){
_sort[section] = {key, dir: cur.dir==='asc' ? 'desc' : 'asc'};
} else {
_sort[section] = {key, dir: 'asc'};
}
// Re-render
switch(section){
case 'savezi': return applySaveziFilter();
case 'klubovi': return applyKluboviFilter();
case 'sportasi': return applySportasiFilter();
case 'objekti': return applyObjektiFilter();
case 'manifestacije': return applyManifFilter();
case 'financije': return refreshFinancije();
}
}
function sortHeader(section, key, label, klass){
const s = _sort[section];
const isCur = s && s.key === key;
const arrow = isCur ? (s.dir==='asc' ? ' ▲' : ' ▼') : '';
const cls = (klass||'') + ' sortable';
return '<th class="'+cls+'" onclick="setSort(\''+section+'\',\''+key+'\')" style="cursor:pointer;user-select:none">'+esc(label)+'<span style="color:var(--pgz-gold);font-weight:700">'+arrow+'</span></th>';
}
function debounce(fn, ms){
let t;
return function(){
const args = arguments;
clearTimeout(t);
t = setTimeout(()=>fn.apply(null, args), ms);
};
}
//=========== PANEL ===========
function openPanel(title, html){
const t = $('#panel-hdr-t');
if(t) t.textContent = title || 'Detalji';
const b = $('#panel-body');
if(b) b.innerHTML = html;
$('#panel').classList.add('open');
$('#panel-overlay').classList.add('open');
}
function closePanel(){
$('#panel').classList.remove('open');
$('#panel-overlay').classList.remove('open');
}
document.addEventListener('keydown', e => { if(e.key==='Escape') closePanel(); });
//=========== NAVIGATION ===========
function buildNav(){
const nav = $('#nav');
nav.innerHTML = NAV_ITEMS.map(n => '<div class="nav-i '+(n.id===_state.section?'active':'')+'" data-id="'+n.id+'" data-label="'+n.label+'" onclick="navTo(\''+n.id+'\')"><span class="ic">'+n.ic+'</span><span class="lbl">'+n.label+'</span></div>').join('');
}
function toggleSidebar(){
const sb = document.getElementById('sb');
const tg = document.getElementById('sb-toggle');
if(!sb) return;
const isCollapsed = sb.classList.toggle('collapsed');
if(tg) tg.textContent = '≡';
try { localStorage.setItem('sidebar-state', isCollapsed ? 'collapsed' : 'expanded'); } catch(e){}
}
function restoreSidebar(){
try {
const s = localStorage.getItem('sidebar-state');
if(s === 'collapsed'){
const sb = document.getElementById('sb');
const tg = document.getElementById('sb-toggle');
if(sb) sb.classList.add('collapsed');
if(tg) tg.textContent = '≡';
}
} catch(e){}
}
function navTo(id){
_state.section = id;
$$('.nav-i').forEach(el => el.classList.toggle('active', el.dataset.id===id));
$$('.section').forEach(el => el.classList.remove('active'));
const sec = $('#pg-'+id);
if(sec) sec.classList.add('active');
const t = SECTION_TITLES[id] || [id, ''];
$('#tb-t').textContent = t[0];
$('#tb-s').textContent = t[1];
loadSection(id);
}
function loadSection(id){
switch(id){
case 'dashboard': return loadDash();
case 'savezi': return loadSavezi();
case 'klubovi': return loadKlubovi();
case 'sportasi': return loadSportasi();
case 'financije': return loadFinancije();
case 'objekti': return loadObjekti();
case 'manifestacije': return loadManifestacije();
case 'mreza': return loadMreza();
case 'forenzika': return loadForenzika();
}
}
//=========== DASHBOARD ===========
async function loadDash(){
const root = $('#pg-dashboard');
root.innerHTML = '<div class="loading">Učitavanje dashboard podataka…</div>';
const d = await api('/dashboard');
if(!d){ root.innerHTML='<div class="empty">Greška pri dohvatu podataka</div>'; return; }
_cache.dash = d;
const proracun2026 = (d.proracun_trend||[]).filter(x=>x.godina===2026)[0];
const total2026 = proracun2026 ? proracun2026.ukupno : d.proracun_aktualni;
root.innerHTML = `
<div class="kpi-grid">
<div class="kpi"><div class="kpi-l">Saveza</div><div class="kpi-v">${fmtNum(d.aktivnih_saveza)}</div><div class="kpi-s">aktivnih</div></div>
<div class="kpi b"><div class="kpi-l">Klubova</div><div class="kpi-v">${fmtNum(d.aktivnih_klubova)}</div><div class="kpi-s">${d.nositelja_kvalitete||0} nositelja kvalitete</div></div>
<div class="kpi g"><div class="kpi-l">Sportaša</div><div class="kpi-v">${fmtNum(d.aktivnih_clanova)}</div><div class="kpi-s">${d.reprezentativaca||0} reprezentativaca</div></div>
<div class="kpi"><div class="kpi-l">Proračun ${proracun2026?proracun2026.godina:2026}</div><div class="kpi-v">${fmtEur(total2026)}</div><div class="kpi-s">javne potrebe u sportu</div></div>
<div class="kpi r"><div class="kpi-l">Kritičnih alarma</div><div class="kpi-v">${fmtNum(d.critical_alerts)}</div><div class="kpi-s">${d.warning_alerts||0} upozorenja</div></div>
</div>
<div class="row-2">
<div class="card">
<div class="card-h"><div class="card-t">📈 Proračun za sport po godinama</div><div class="tb-s">Klikni godinu za detalje (PDF dokumenti)</div></div>
<div class="chart-box"><canvas id="chProracun"></canvas></div>
</div>
<div class="card">
<div class="card-h"><div class="card-t">🏅 Top savezi po broju registriranih</div><div class="tb-s">Klikni za drill-down</div></div>
<div style="overflow-x:auto;max-height:340px;overflow-y:auto"><table>
<thead><tr><th>Savez</th><th class="num">Klubova</th><th class="num">Reg.</th><th class="num">Trenera</th><th class="num">Repr.</th></tr></thead>
<tbody>${(d.top_savezi||[]).slice(0,10).map(s => `
<tr onclick="openSavezByName('${esc(s.naziv).replace(/&#39;/g,"\\'")}')">
<td><b>${esc(s.naziv)}</b></td>
<td class="num">${fmtNum(s.klubova_clanica)}</td>
<td class="num">${fmtNum(s.registriranih)}</td>
<td class="num">${fmtNum(s.trenera)}</td>
<td class="num">${fmtNum(s.reprezentativaca)}</td>
</tr>`).join('')}</tbody>
</table></div>
</div>
</div>
<div class="card" style="margin-top:14px">
<div class="card-h">
<div class="card-t">💰 Najveći primatelji javnih potreba</div>
<div style="display:flex;gap:8px;align-items:center">
<select id="dash-god" onchange="refreshDashNositelji()" style="background:var(--bg2);border:1px solid var(--rim);border-radius:5px;padding:6px 10px;color:var(--t1);font-size:12px">
<option value="2026">2026</option>
<option value="2025" selected>2025</option>
<option value="2024">2024</option>
</select>
<span class="tb-s" id="dash-nos-cnt"></span>
</div>
</div>
<div id="dash-nos-out"><div class="loading">Učitavanje…</div></div>
</div>
`;
drawProracunChart(d.proracun_trend || []);
refreshDashNositelji();
}
async function refreshDashNositelji(){
const sel = $('#dash-god');
if(!sel) return;
const god = sel.value;
const out = $('#dash-nos-out');
out.innerHTML = '<div class="loading">Učitavanje primatelja '+god+'…</div>';
const d = await api('/v2/potpore/by-year?godina='+god);
if(!d){ out.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
const rows = (d.results || []).slice().sort((a,b)=>Number(b.iznos_eur||0)-Number(a.iznos_eur||0)).slice(0, 25);
$('#dash-nos-cnt').textContent = rows.length+' / '+(d.count||0)+' · ukupno '+fmtEur(d.total||0);
out.innerHTML = `<div style="overflow-x:auto"><table>
<thead><tr><th>#</th><th>Korisnik</th><th>Sport</th><th>Vrsta</th><th class="num">Iznos</th><th>PDF</th></tr></thead>
<tbody>${rows.map((r,i) => `
<tr onclick='openPrimateljDetail(${JSON.stringify(r).replace(/'/g,"&#39;")})'>
<td>${i+1}</td>
<td><b>${esc(r.korisnik)}</b></td>
<td>${txt(r.sport)}</td>
<td>${txt(r.vrsta)}</td>
<td class="num"><b>${fmtEurFull(r.iznos_eur)}</b></td>
<td>${r.source_url?'<a href="'+esc(r.source_url)+'" target="_blank" onclick="event.stopPropagation()">📄 PDF</a>':'—'}</td>
</tr>`).join('')}</tbody>
</table></div>`;
}
async function openSavezByName(name){
const list = _cache.savezi || (await api('/savezi?limit=250'))?.rows || [];
if(!_cache.savezi) _cache.savezi = list;
const target = list.find(s => s.naziv === name) || list.find(s => (s.naziv||'').toLowerCase() === name.toLowerCase());
if(target) return openSavez(target.id);
// Try fuzzy match by first two words
const prefix = name.split(' ').slice(0,2).join(' ').toLowerCase();
const fuzzy = list.find(s => (s.naziv||'').toLowerCase().includes(prefix));
if(fuzzy) return openSavez(fuzzy.id);
openPanel('Savez', '<div class="empty">Savez <b>'+esc(name)+'</b> nije pronađen u bazi.</div>');
}
async function openPrimateljDetail(r){
openPanel('Primatelj', '<div class="loading">Učitavanje detalja…</div>');
// Try to find matching klub by name
let klub = null;
if(_cache.klubovi){
const lower = (r.korisnik||'').toLowerCase();
klub = _cache.klubovi.find(k => (k.klub||'').toLowerCase() === lower);
if(!klub) klub = _cache.klubovi.find(k => (k.klub||'').toLowerCase().includes(lower) || lower.includes((k.klub||'').toLowerCase()));
}
if(klub){
return openKlub(klub.id);
}
// Fallback panel — show row data + PDF + same-year other primatelji from same source
const html = `
<div class="card-h" style="border:0;padding:0;margin-bottom:14px">
<div>
<div style="font-size:18px;font-weight:800;color:var(--t0)">${esc(r.korisnik)}</div>
<div style="font-size:12px;color:var(--t2);margin-top:4px">Primatelj proračuna ${r.godina}</div>
</div>
</div>
<div class="card">
<div class="card-h"><div class="card-t">📋 Detalji isplate</div></div>
<div class="kv">
<div class="k">Korisnik</div><div class="v">${esc(r.korisnik)}</div>
<div class="k">Sport</div><div class="v">${txt(r.sport)}</div>
<div class="k">Vrsta</div><div class="v">${txt(r.vrsta)}</div>
<div class="k">Iznos</div><div class="v"><b style="color:var(--pgz-gold);font-size:16px">${fmtEurFull(r.iznos_eur)}</b></div>
<div class="k">Godina</div><div class="v">${esc(r.godina)}</div>
<div class="k">Izvor</div><div class="v">${txt(r.izvor)}</div>
<div class="k">Napomena</div><div class="v">${txt(r.napomena)}</div>
</div>
${r.source_url ? '<div style="margin-top:14px;text-align:center"><a href="'+esc(r.source_url)+'" target="_blank" class="btn primary" style="display:inline-block;text-decoration:none">📄 Otvori izvorni PDF dokument</a></div>' : '<div class="empty" style="margin-top:14px">PDF dokument nije dostupan za ovu stavku.</div>'}
</div>
<div class="card">
<div class="card-h"><div class="card-t">🔍 Klub nije povezan</div></div>
<div style="font-size:12px;color:var(--t2);line-height:1.5">
Korisnik <b>${esc(r.korisnik)}</b> nije automatski povezan s pojedinim klubom u bazi.
Provjeri klub ručno preko sekcije <a href="javascript:navTo('klubovi')">Klubovi</a> ili pretraži primatelja na izvornom dokumentu.
</div>
</div>
`;
openPanel('Primatelj · '+r.korisnik, html);
}
function drawProracunChart(trend){
const ctx = $('#chProracun');
if(!ctx) return;
if(_proracunChart){ _proracunChart.destroy(); _proracunChart=null; }
_proracunChart = new Chart(ctx, {
type:'bar',
data:{
labels: trend.map(x=>x.godina),
datasets:[{
label:'EUR',
data: trend.map(x=>x.ukupno),
backgroundColor: trend.map(x=> x.godina===2026 ? '#F4C430' : '#004CC4'),
borderRadius:4
}]
},
options:{
responsive:true, maintainAspectRatio:false,
plugins:{
legend:{display:false},
tooltip:{callbacks:{label:c => '€'+Number(c.parsed.y).toLocaleString('hr-HR')}}
},
scales:{
x:{ticks:{color:'#8a95b4'}, grid:{display:false}},
y:{ticks:{color:'#8a95b4', callback:v=>'€'+(v/1000)+'k'}, grid:{color:'#1e2a50'}}
},
onClick:(e, els) => {
if(els && els.length){
const i = els[0].index;
openProracunDrill(trend[i].godina, trend[i].ukupno);
}
}
}
});
}
async function openProracunDrill(godina, total){
openPanel('Proračun '+godina, '<div class="loading">Dohvaćanje primatelja…</div>');
const d = await api('/v2/potpore/by-year?godina='+godina);
if(!d){ openPanel('Proračun '+godina, '<div class="empty">Greška pri dohvatu</div>'); return; }
const rows = d.results || [];
const grandTotal = d.total || total || 0;
const html = `
<div class="kpi-grid" style="grid-template-columns:1fr 1fr">
<div class="kpi"><div class="kpi-l">Ukupno ${godina}</div><div class="kpi-v">${fmtEur(grandTotal)}</div></div>
<div class="kpi b"><div class="kpi-l">Primatelja</div><div class="kpi-v">${rows.length}</div></div>
</div>
<div style="overflow-x:auto"><table>
<thead><tr><th>#</th><th>Korisnik</th><th>Sport</th><th class="num">Iznos</th><th>PDF</th></tr></thead>
<tbody>
${rows.map((r,i) => `
<tr class="no-click">
<td>${i+1}</td>
<td>${esc(r.korisnik)}</td>
<td>${txt(r.sport)}</td>
<td class="num"><b>${fmtEurFull(r.iznos_eur)}</b></td>
<td>${r.source_url ? '<a href="'+esc(r.source_url)+'" target="_blank">📄</a>' : '—'}</td>
</tr>`).join('')}
</tbody>
</table></div>
`;
openPanel('Primatelji proračuna · '+godina, html);
}
//=========== SAVEZI ===========
async function loadSavezi(){
const root = $('#pg-savezi');
if(!_cache.savezi){
root.innerHTML = '<div class="loading">Učitavanje saveza…</div>';
const d = await api('/savezi?limit=250');
if(!d){ root.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
_cache.savezi = d.rows || [];
}
renderSaveziShell();
applySaveziFilter();
}
function renderSaveziShell(){
const root = $('#pg-savezi');
root.innerHTML = `
<div class="toolbar">
<input type="search" id="sav-q" placeholder="🔍 Pretraži savez…">
<select id="sav-pgz">
<option value="">Svi savezi</option>
<option value="1">Samo PGŽ relevantni</option>
</select>
<div class="toggle">
<button id="sav-card" class="${_state.viewSavezi==='card'?'active':''}" onclick="setSaveziView('card')">Kartice</button>
<button id="sav-table" class="${_state.viewSavezi==='table'?'active':''}" onclick="setSaveziView('table')">Tablica</button>
</div>
<span class="tb-s" id="sav-cnt"></span>
</div>
<div id="sav-out"></div>
`;
$('#sav-q').addEventListener('input', debounce(applySaveziFilter, 200));
$('#sav-pgz').addEventListener('change', applySaveziFilter);
}
function setSaveziView(v){
_state.viewSavezi = v;
$('#sav-card').classList.toggle('active', v==='card');
$('#sav-table').classList.toggle('active', v==='table');
applySaveziFilter();
}
function applySaveziFilter(){
const q = (($('#sav-q')?$('#sav-q').value:'') || '').toLowerCase().trim();
const pgz = $('#sav-pgz') ? $('#sav-pgz').value : '';
let rows = _cache.savezi || [];
if(q) rows = rows.filter(s => (s.naziv||'').toLowerCase().includes(q) || (s.sport||'').toLowerCase().includes(q));
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';
$('#sav-out').innerHTML = _state.viewSavezi==='card' ? renderSaveziGrid(rows) : renderSaveziTable(rows);
}
function renderSaveziGrid(rows){
if(!rows.length) return '<div class="empty">Nema rezultata</div>';
return '<div class="grid">'+rows.map(s => `
<div class="entity" onclick="openSavez(${s.id})">
${s.pgz_relevant?'<div class="et-tag">PGŽ</div>':''}
<div class="et">${esc(s.naziv)}</div>
<div class="es">${txt(s.sport,'—')} · ${txt(s.predsjednik,'bez predsjednika')}</div>
<div class="em">
<span><b>${fmtNum(s.broj_klubova)}</b> klubova</span>
<span><b>${fmtNum(s.reg_2024)}</b> reg.</span>
<span><b>${fmtNum(s.treneri_2024)}</b> trenera</span>
</div>
</div>`).join('')+'</div>';
}
function renderSaveziTable(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('savezi','naziv','Naziv','')}${sortHeader('savezi','sport','Sport','')}${sortHeader('savezi','predsjednik','Predsjednik','')}${sortHeader('savezi','email','Email','')}${sortHeader('savezi','broj_klubova','Klubova','num')}${sortHeader('savezi','reg_2024','Reg.','num')}${sortHeader('savezi','pgz_relevant','PGŽ','')}</tr></thead>
<tbody>${rows.map(s => `
<tr onclick="openSavez(${s.id})">
<td><b>${esc(s.naziv)}</b></td>
<td>${txt(s.sport)}</td>
<td>${txt(s.predsjednik)}</td>
<td>${s.email?'<span class="tag b">'+esc(s.email)+'</span>':'—'}</td>
<td class="num">${fmtNum(s.broj_klubova)}</td>
<td class="num">${fmtNum(s.reg_2024)}</td>
<td>${s.pgz_relevant?'<span class="tag gd">PGŽ</span>':'—'}</td>
</tr>`).join('')}</tbody>
</table></div>`;
}
async function openSavez(id){
openPanel('Savez', '<div class="loading">Učitavanje saveza…</div>');
const s = await api('/savezi/'+id);
if(!s || s.detail){
openPanel('Savez', '<div class="empty">Savez nije pronađen</div>');
return;
}
const ksearch = (s.naziv||'').split(' ').slice(0,2).join(' ');
const kr = await api('/klubovi?savez='+encodeURIComponent(ksearch)+'&limit=200');
const klubovi = (kr && kr.rows) ? kr.rows.filter(k => (k.savez||'').toLowerCase().includes(ksearch.toLowerCase())) : [];
const html = `
<div class="card-h" style="border:0;padding:0;margin-bottom:14px">
<div>
<div style="font-size:18px;font-weight:800;color:var(--t0)">${esc(s.naziv)}</div>
<div style="font-size:12px;color:var(--t2);margin-top:4px">${txt(s.sport,'—')} · ${s.region||'PGŽ'}</div>
</div>
</div>
<div class="kpi-grid" style="grid-template-columns:repeat(3,1fr);margin-bottom:14px">
<div class="kpi"><div class="kpi-l">Klubova</div><div class="kpi-v">${klubovi.length}</div></div>
<div class="kpi b"><div class="kpi-l">Godina osnutka</div><div class="kpi-v" style="font-size:18px">${txt(s.godina_osnutka)}</div></div>
<div class="kpi g"><div class="kpi-l">Status</div><div class="kpi-v" style="font-size:14px">${s.aktivan?'AKTIVAN':'NEAKTIVAN'}</div></div>
</div>
<div class="card">
<div class="card-h"><div class="card-t">📋 Osnovne informacije</div></div>
<div class="kv">
<div class="k">OIB</div><div class="v">${txt(s.oib)}</div>
<div class="k">Adresa</div><div class="v">${txt(s.adresa)}</div>
<div class="k">Predsjednik</div><div class="v">${txt(s.predsjednik)}</div>
<div class="k">Tajnik</div><div class="v">${txt(s.tajnik)}</div>
<div class="k">Email</div><div class="v">${s.email?'<a href="mailto:'+esc(s.email)+'">'+esc(s.email)+'</a>':'—'}</div>
<div class="k">Telefon</div><div class="v">${txt(s.telefon)}</div>
<div class="k">Web</div><div class="v">${s.web?'<a href="'+esc(s.web)+'" target="_blank">'+esc(s.web)+'</a>':'—'}</div>
<div class="k">IBAN</div><div class="v">${txt(s.iban)}</div>
</div>
</div>
<div class="card">
<div class="card-h"><div class="card-t">⬡ Klubovi članice (${klubovi.length})</div></div>
${klubovi.length ? `<div style="overflow-x:auto;max-height:400px;overflow-y:auto"><table>
<thead><tr><th>Klub</th><th>Razina</th><th>Grad</th></tr></thead>
<tbody>${klubovi.slice(0,100).map(k => `
<tr onclick="closePanel();setTimeout(()=>openKlub(${k.id}),250)">
<td>${esc(k.klub||k.sport||'(bez naziva)')}${k.nositelj_kvalitete?' <span class="tag gd">N.K.</span>':''}</td>
<td>${txt(k.razina,'')}</td>
<td>${txt(k.grad)}</td>
</tr>`).join('')}
</tbody>
</table></div>` : '<div class="empty">Nema podataka o klubovima</div>'}
</div>
${enrichBlock('savez', s.id)}
`;
openPanel('Savez · '+s.naziv, html);
}
//=========== KLUBOVI ===========
async 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');
if(!d){ root.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
_cache.klubovi = d.rows || [];
}
renderKluboviShell();
applyKluboviFilter();
}
function renderKluboviShell(){
const root = $('#pg-klubovi');
const sports = Array.from(new Set((_cache.klubovi||[]).map(k=>k.sport).filter(Boolean))).sort().slice(0,80);
const grads = Array.from(new Set((_cache.klubovi||[]).map(k=>k.grad).filter(Boolean))).sort();
root.innerHTML = `
<div class="toolbar">
<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>
<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>
<span class="tb-s" id="kl-cnt"></span>
</div>
<div id="kl-out"></div>
`;
$('#kl-q').addEventListener('input', debounce(applyKluboviFilter, 200));
$('#kl-sport').addEventListener('change', applyKluboviFilter);
$('#kl-grad').addEventListener('change', applyKluboviFilter);
$('#kl-nk').addEventListener('change', applyKluboviFilter);
}
function setKluboviView(v){
_state.viewKlubovi = v;
$('#kl-card').classList.toggle('active', v==='card');
$('#kl-table').classList.toggle('active', v==='table');
applyKluboviFilter();
}
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 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(_sort.klubovi) rows = sortRows(rows, _sort.klubovi.key, _sort.klubovi.dir);
$('#kl-cnt').textContent = rows.length+' klubova';
const top = rows.slice(0, 300);
$('#kl-out').innerHTML = _state.viewKlubovi==='card' ? renderKluboviGrid(top) : renderKluboviTable(top);
if(rows.length>300){
$('#kl-out').insertAdjacentHTML('beforeend', '<div class="empty">… i još '+(rows.length-300)+' klubova. Suzite filtre.</div>');
}
}
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>':''}
<div class="et">${esc(k.klub||k.sport||'(bez naziva)')}</div>
<div class="es">${txt(k.razina,'')} · ${txt(k.grad,'—')}</div>
<div class="em">
<span><b>${fmtNum(k.registriranih)}</b> reg.</span>
<span><b>${fmtNum(k.trenera)}</b> trenera</span>
<span><b>${fmtNum(k.reprezentativaca)}</b> repr.</span>
</div>
</div>`).join('')+'</div>';
}
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>
<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>`).join('')}</tbody>
</table></div>`;
}
async function openKlub(id){
openPanel('Klub', '<div class="loading">Učitavanje kluba…</div>');
const k = await api('/klubovi/'+id);
if(!k || k.detail){
openPanel('Klub', '<div class="empty">Klub nije pronađen</div>');
return;
}
const stats = k.stats || {};
const clanovi = k.clanovi || [];
const potpore = k.potpore || [];
let scoreCount = 0;
if(k.oib) scoreCount++;
if(k.predsjednik) scoreCount++;
if(k.tajnik) scoreCount++;
if(k.email) scoreCount++;
if(k.web || k.web_stranica) scoreCount++;
if(k.sjediste || k.adresa) scoreCount++;
if(k.ciljevi) scoreCount++;
if(k.opis_djelatnosti) scoreCount++;
const scoreClass = scoreCount>=6?'high':(scoreCount>=4?'mid':'low');
const html = `
<div class="card-h" style="border:0;padding:0;margin-bottom:14px">
<div>
<div style="font-size:18px;font-weight:800;color:var(--t0)">${esc(k.naziv||'(bez naziva)')}</div>
<div style="font-size:12px;color:var(--t2);margin-top:4px">${txt(k.sport,'—')} · ${txt(k.razina,'')} · ${txt(k.grad,'')}</div>
</div>
</div>
<div class="kpi-grid" style="grid-template-columns:repeat(4,1fr);margin-bottom:14px">
<div class="kpi"><div class="kpi-l">Sportaša</div><div class="kpi-v">${fmtNum(stats.broj_clanova||clanovi.length)}</div></div>
<div class="kpi b"><div class="kpi-l">Registriranih</div><div class="kpi-v">${fmtNum(stats.broj_registriranih)}</div></div>
<div class="kpi g"><div class="kpi-l">Trenera</div><div class="kpi-v">${fmtNum(stats.broj_trenera)}</div></div>
<div class="kpi"><div class="kpi-l">Score baze</div><div class="kpi-v" style="font-size:18px"><span class="score ${scoreClass}">${scoreCount}/8</span></div></div>
</div>
<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-pot')">Potpore (${potpore.length})</div>
</div>
<div id="k-info" class="ktab">
<div class="kv">
<div class="k">Naziv</div><div class="v">${esc(k.naziv||'')}</div>
<div class="k">OIB</div><div class="v">${txt(k.oib)}</div>
<div class="k">Sport</div><div class="v">${txt(k.sport)}</div>
<div class="k">Razina</div><div class="v">${txt(k.razina)}</div>
<div class="k">Savez</div><div class="v">${txt(k.savez_naziv)}</div>
<div class="k">Predsjednik</div><div class="v">${txt(k.predsjednik)}</div>
<div class="k">Tajnik</div><div class="v">${txt(k.tajnik)}</div>
<div class="k">Trener</div><div class="v">${txt(k.trener_glavni)}</div>
<div class="k">Sjedište</div><div class="v">${txt(k.sjediste||k.adresa)}</div>
<div class="k">Grad</div><div class="v">${txt(k.grad)}</div>
<div class="k">Email</div><div class="v">${k.email?'<a href="mailto:'+esc(k.email)+'">'+esc(k.email)+'</a>':'—'}</div>
<div class="k">Telefon</div><div class="v">${txt(k.telefon)}</div>
<div class="k">Web</div><div class="v">${(k.web||k.web_stranica)?'<a href="'+esc(k.web||k.web_stranica)+'" target="_blank">'+esc(k.web||k.web_stranica)+'</a>':'—'}</div>
<div class="k">Osnovan</div><div class="v">${txt(k.godina_osnutka)}</div>
<div class="k">Nositelj kvalitete</div><div class="v">${k.nositelj_kvalitete?'<span class="tag gd">DA</span>':'<span class="tag">NE</span>'}</div>
</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>
<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>${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-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>
<tbody>${potpore.map(p => `
<tr class="no-click">
<td>${esc(p.godina)}</td>
<td>${esc(p.naziv_kluba||'')}</td>
<td class="num"><b>${fmtEurFull(p.iznos)}</b></td>
</tr>`).join('')}
</tbody>
</table></div>` : '<div class="empty">Nema zabilježenih potpora</div>'}
</div>
${enrichBlock('klub', k.id)}
`;
openPanel('Klub · '+(k.naziv||''), html);
}
function switchKlubTab(el, tabId){
const parent = el.parentElement;
parent.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
el.classList.add('active');
parent.parentElement.querySelectorAll('.ktab').forEach(t=>t.style.display='none');
const target = document.getElementById(tabId);
if(target) target.style.display='block';
}
//=========== SPORTAŠI ===========
async function loadSportasi(){
const root = $('#pg-sportasi');
if(!_cache.clanovi){
root.innerHTML = '<div class="loading">Učitavanje sportaša…</div>';
const d = await api('/clanovi-full?limit=500');
if(!d){ root.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
_cache.clanovi = d.rows || [];
}
renderSportasiShell();
applySportasiFilter();
}
function renderSportasiShell(){
const root = $('#pg-sportasi');
root.innerHTML = `
<div class="toolbar">
<input type="search" id="sp-q" placeholder="🔍 Ime ili prezime…">
<select id="sp-hoo">
<option value="">Sve HOO kategorije</option>
<option value="1">I. kategorija</option>
<option value="2">II. kategorija</option>
<option value="3">III. kategorija</option>
<option value="4">IV. kategorija</option>
<option value="5">V. kategorija</option>
</select>
<label><input type="checkbox" id="sp-rep"> Samo reprezentativci</label>
<label><input type="checkbox" id="sp-foto"> Samo s fotografijom</label>
<div class="toggle">
<button id="sp-card" class="${_state.viewSportasi==='card'?'active':''}" onclick="setSportasiView('card')">Kartice</button>
<button id="sp-table" class="${_state.viewSportasi==='table'?'active':''}" onclick="setSportasiView('table')">Tablica</button>
</div>
<span class="tb-s" id="sp-cnt"></span>
<button class="btn primary" onclick="openSportas(449)">⭐ Test: Josip Zec</button>
</div>
<div id="sp-out"></div>
`;
$('#sp-q').addEventListener('input', debounce(applySportasiFilter, 200));
$('#sp-hoo').addEventListener('change', applySportasiFilter);
$('#sp-rep').addEventListener('change', applySportasiFilter);
$('#sp-foto').addEventListener('change', applySportasiFilter);
}
function openOIB(oib){
const cleanOib = String(oib||'').replace(/[^0-9]/g,'');
const url = 'https://sudreg.pravosudje.hr/registar/oc/index.html#osnovniPodaci?o='+encodeURIComponent(cleanOib);
// Open external — sudreg supports OIB lookup
window.open(url, '_blank', 'noopener');
}
function filterKluboviByCity(grad){
closePanel();
navTo('klubovi');
setTimeout(() => {
const sel = $('#kl-grad');
if(sel){ sel.value = grad; applyKluboviFilter(); }
}, 100);
}
function filterKluboviBySport(sport){
closePanel();
navTo('klubovi');
setTimeout(() => {
const sel = $('#kl-sport');
if(sel){ sel.value = sport; applyKluboviFilter(); }
}, 100);
}
function filterObjektiByCity(grad){
closePanel();
navTo('objekti');
setTimeout(() => {
const sel = $('#ob-grad');
if(sel){ sel.value = grad; applyObjektiFilter(); }
}, 100);
}
function filterSportasiBy(field, value){
closePanel();
navTo('sportasi');
setTimeout(() => {
if(field === 'sport'){
// Use search box since there's no sport dropdown
const q = $('#sp-q'); if(q){ q.value = value; }
} else if(field === 'mjesto_rodjenja' || field === 'grad'){
const q = $('#sp-q'); if(q){ q.value = value; }
} else if(field === 'reprezentativac'){
const cb = $('#sp-rep'); if(cb){ cb.checked = !!value; }
} else if(field === 'hoo'){
const sel = $('#sp-hoo'); if(sel){ sel.value = String(value); }
} else if(field === 'aktivan'){
// Add to extra-filters slot if exists; else search by status string
_state.spExtraAktivan = value ? 'true' : 'false';
} else if(field === 'stipendiran'){
_state.spExtraStipendiran = !!value;
}
applySportasiFilter();
}, 100);
}
function filterSportasiByYear(year){
closePanel();
navTo('sportasi');
setTimeout(() => {
_state.spYear = String(year);
applySportasiFilter();
}, 100);
}
function clearSportasiExtras(){
_state.spExtraAktivan = '';
_state.spExtraStipendiran = false;
_state.spYear = '';
}
function setSportasiView(v){
_state.viewSportasi = v;
$('#sp-card').classList.toggle('active', v==='card');
$('#sp-table').classList.toggle('active', v==='table');
applySportasiFilter();
}
function applySportasiFilter(){
const q = (($('#sp-q')?$('#sp-q').value:'') || '').toLowerCase().trim();
const hoo = $('#sp-hoo') ? $('#sp-hoo').value : '';
const rep = $('#sp-rep') ? $('#sp-rep').checked : false;
const foto = $('#sp-foto') ? $('#sp-foto').checked : false;
let rows = _cache.clanovi || [];
if(q) rows = rows.filter(c => ((c.ime||'')+' '+(c.prezime||'')).toLowerCase().includes(q));
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);
if(_state.spYear){
rows = rows.filter(c => String(c.datum_rodenja||c.datum_rodjenja||'').slice(0,4) === _state.spYear);
}
if(_state.spExtraAktivan==='true') rows = rows.filter(c => c.aktivan);
if(_state.spExtraAktivan==='false') rows = rows.filter(c => !c.aktivan);
if(_state.spExtraStipendiran) rows = rows.filter(c => c.stipendiran);
if(_sort.sportasi) rows = sortRows(rows, _sort.sportasi.key, _sort.sportasi.dir);
$('#sp-cnt').textContent = rows.length+' sportaša';
const top = rows.slice(0, 300);
$('#sp-out').innerHTML = _state.viewSportasi==='card' ? renderSportasiGrid(top) : renderSportasiTable(top);
if(rows.length>300){
$('#sp-out').insertAdjacentHTML('beforeend', '<div class="empty">… i još '+(rows.length-300)+' sportaša. Suzite filtre.</div>');
}
}
function renderSportasiGrid(rows){
if(!rows.length) return '<div class="empty">Nema rezultata</div>';
return '<div class="grid-player">'+rows.map(c => buildPlayerCard(c)).join('')+'</div>';
}
function buildPlayerCard(c){
const initials = (((c.ime||'?')[0]||'?')+((c.prezime||'?')[0]||'?')).toUpperCase();
const photo = c.slika_url ? '<img src="'+esc(c.slika_url)+'" alt="" onerror="this.style.display=\'none\';if(this.parentElement)this.parentElement.innerHTML=\'<div class=\\\'no\\\'>'+initials+'</div>\'">' : '<div class="no">'+initials+'</div>';
const hooCat = c.hoo_kategorija || c.kategorija_hoo;
return `
<div class="player-card" onclick="openSportas(${c.id})">
<div class="ph">${photo}</div>
<div class="pb">
<div class="pn">${esc(c.ime||'')} ${esc(c.prezime||'')}</div>
<div class="pp">${txt(c.sport,'—')} · ${txt(c.pozicija,'')}</div>
<div class="pk">${txt(c.klub_naziv_godisnjak,'')}</div>
<div class="badges">
${c.reprezentativac?'<span class="badge repr">REPR</span>':''}
${hooCat?'<span class="badge hoo">HOO '+esc(hooCat)+'</span>':''}
${c.stipendiran?'<span class="badge">STIP</span>':''}
${c.broj_dresa?'<span class="badge">#'+esc(c.broj_dresa)+'</span>':''}
</div>
</div>
</div>`;
}
function renderSportasiTable(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('sportasi','prezime','Prezime','')}${sortHeader('sportasi','ime','Ime','')}${sortHeader('sportasi','sport','Sport','')}${sortHeader('sportasi','pozicija','Pozicija','')}${sortHeader('sportasi','hoo_kategorija','HOO','')}${sortHeader('sportasi','reprezentativac','Repr.','')}${sortHeader('sportasi','slika_url','Foto','')}</tr></thead>
<tbody>${rows.map(c => `
<tr onclick="openSportas(${c.id})">
<td><b>${esc(c.prezime||'')}</b></td>
<td>${esc(c.ime||'')}</td>
<td>${txt(c.sport)}</td>
<td>${txt(c.pozicija)}</td>
<td>${(c.hoo_kategorija||c.kategorija_hoo)?'<span class="tag b">'+esc(c.hoo_kategorija||c.kategorija_hoo)+'</span>':'—'}</td>
<td>${c.reprezentativac?'<span class="tag gd">DA</span>':'—'}</td>
<td>${c.slika_url?'📸':'—'}</td>
</tr>`).join('')}</tbody>
</table></div>`;
}
async function openSportas(id){
openPanel('Sportaš', '<div class="loading">Učitavanje profila…</div>');
const d = await api('/sportas/'+id+'/profil');
if(!d || d.detail){
openPanel('Sportaš', '<div class="empty">Sportaš nije pronađen</div>');
return;
}
const stats = d.stats || {};
const sezone = d.clan_sezona || [];
const utakmice = d.utakmice || [];
const nagrade = d.nagrade || [];
const godisnjaci = d.godisnjak_godine || d.godisnjaci || [];
const initials = (((d.ime||'?')[0]||'?')+((d.prezime||'?')[0]||'?')).toUpperCase();
const photo = d.slika_url ? '<img src="'+esc(d.slika_url)+'" alt="" onerror="this.style.display=\'none\';if(this.parentElement)this.parentElement.innerHTML=\'<div class=\\\'no\\\'>'+initials+'</div>\'">' : '<div class="no">'+initials+'</div>';
const dob = d.datum_rodjenja || d.datum_rodenja;
const hooCat = d.hoo_kategorija || d.kategorija_hoo;
const html = `
<div class="pp-hdr">
<div class="pp-foto">${photo}</div>
<div class="pp-info">
<div class="pp-name">${esc(d.ime||'')} ${esc(d.prezime||'')}</div>
<div class="pp-meta">
${d.sport?'<a class="link-chip" onclick="filterSportasiBy(&quot;sport&quot;,&quot;'+esc(d.sport)+'&quot;)">'+esc(d.sport)+'</a>':'—'} ·
${txt(d.pozicija,'')} ·
${d.klub_id ? '<a class="link-chip" onclick="closePanel();setTimeout(()=>openKlub('+d.klub_id+'),250)"><b>'+esc(d.klub_naziv_full||d.klub_naziv_godisnjak||'—')+'</b></a>' : '<b>'+esc(d.klub_naziv_full||d.klub_naziv_godisnjak||'—')+'</b>'}
</div>
<div class="pp-meta">
${dob ? '<a class="link-chip" onclick="filterSportasiByYear(&quot;'+esc((dob||'').slice(0,4))+'&quot;)">📅 '+fmtDate(dob)+'</a>' : '📅 —'}
${(d.mjesto_rodjenja||d.mjesto_rodenja)?' · <a class="link-chip" onclick="filterSportasiBy(&quot;mjesto_rodjenja&quot;,&quot;'+esc(d.mjesto_rodjenja||d.mjesto_rodenja)+'&quot;)">'+esc(d.mjesto_rodjenja||d.mjesto_rodenja)+'</a>':''}
</div>
<div class="pp-tags">
<a class="tag ${d.aktivan?'gr':'rd'}" onclick="filterSportasiBy('aktivan',${d.aktivan?'true':'false'})">${d.aktivan?'AKTIVAN':'NEAKTIVAN'}</a>
${d.reprezentativac?'<a class="tag gd" onclick="filterSportasiBy(&quot;reprezentativac&quot;,true)">REPR</a>':''}
${hooCat?'<a class="tag b" onclick="filterSportasiBy(&quot;hoo&quot;,&quot;'+esc(hooCat)+'&quot;)">HOO '+esc(hooCat)+'</a>':''}
${d.broj_dresa?'<span class="tag">#'+esc(d.broj_dresa)+'</span>':''}
${d.stipendiran?'<a class="tag am" onclick="filterSportasiBy(&quot;stipendiran&quot;,true)">STIP</a>':''}
</div>
</div>
</div>
<div class="pp-stats">
<div class="pp-stat"><div class="v">${fmtNum(stats.ukupno_nastupa||0)}</div><div class="l">Nastupi</div></div>
<div class="pp-stat"><div class="v">${fmtNum(stats.ukupno_pogodaka||0)}</div><div class="l">Golovi</div></div>
<div class="pp-stat"><div class="v">${fmtNum(stats.ukupno_asistencija||0)}</div><div class="l">Asist.</div></div>
<div class="pp-stat"><div class="v" style="color:var(--amber)">${fmtNum(stats.ukupno_zutih||0)}</div><div class="l">Žuti</div></div>
<div class="pp-stat"><div class="v" style="color:var(--red)">${fmtNum(stats.ukupno_crvenih||0)}</div><div class="l">Crveni</div></div>
<div class="pp-stat"><div class="v">${fmtNum(stats.sezone_aktivne||sezone.length)}</div><div class="l">Sezona</div></div>
</div>
<div class="tabs">
<div class="tab active" onclick="switchPlayerTab(this,'p-sez')">Sezone (${sezone.length})</div>
<div class="tab" onclick="switchPlayerTab(this,'p-utak')">Utakmice (${utakmice.length})</div>
<div class="tab" onclick="switchPlayerTab(this,'p-bio')">Bio</div>
<div class="tab" onclick="switchPlayerTab(this,'p-god')">Godišnjaci (${godisnjaci.length})</div>
<div class="tab" onclick="switchPlayerTab(this,'p-nag')">Nagrade (${nagrade.length})</div>
</div>
<div id="p-sez" class="ptab">
${sezone.length ? `<div style="overflow-x:auto"><table>
<thead><tr><th>Sezona</th><th>Natjecanje</th><th>Klub</th><th class="num">Nas.</th><th class="num">Gol.</th><th class="num">Asis.</th><th class="num">Žuti</th><th class="num">Crv.</th><th></th></tr></thead>
<tbody>${sezone.map(s => `
<tr class="no-click">
<td><b>${esc(s.sezona||'')}</b></td>
<td>${esc(s.natjecanje||'')}</td>
<td>${esc(s.klub_naziv||'')}</td>
<td class="num">${fmtNum(s.nastupi)}</td>
<td class="num"><b style="color:var(--pgz-gold)">${fmtNum(s.pogoci)}</b></td>
<td class="num">${fmtNum(s.asistencije)}</td>
<td class="num">${fmtNum(s.zuti_kartoni)}</td>
<td class="num">${fmtNum(s.crveni_kartoni)}</td>
<td>${s.natjecanje_url?'<a href="'+esc(s.natjecanje_url)+'" target="_blank">↗</a>':''}</td>
</tr>`).join('')}
</tbody>
</table></div>` : '<div class="empty">Nema sezonskih podataka</div>'}
</div>
<div id="p-utak" class="ptab" style="display:none">
${utakmice.length ? `<div style="overflow-x:auto;max-height:500px;overflow-y:auto"><table>
<thead><tr><th>Datum</th><th>Natjecanje</th><th colspan="3">Susret</th><th class="num">Gol.</th><th class="num">Min.</th><th></th></tr></thead>
<tbody>${utakmice.map(u => `
<tr class="no-click">
<td>${fmtDate(u.datum)}</td>
<td>${esc(u.natjecanje||'')}</td>
<td>${u.klub_dom_logo?'<img src="'+esc(u.klub_dom_logo)+'" class="utlogo" onerror="this.style.display=\'none\'">':''}${esc(u.klub_dom||'')}</td>
<td><b>${esc(u.rezultat||'-')}</b></td>
<td>${u.klub_gost_logo?'<img src="'+esc(u.klub_gost_logo)+'" class="utlogo" onerror="this.style.display=\'none\'">':''}${esc(u.klub_gost||'')}</td>
<td class="num"><b style="color:var(--pgz-gold)">${fmtNum(u.pogodaka)}</b></td>
<td class="num">${fmtNum(u.minute)}</td>
<td>${u.source_url?'<a href="'+esc(u.source_url)+'" target="_blank">↗</a>':''}</td>
</tr>`).join('')}
</tbody>
</table></div>` : '<div class="empty">Nema podataka o utakmicama</div>'}
</div>
<div id="p-bio" class="ptab" style="display:none">
<div class="kv">
<div class="k">OIB</div><div class="v">${d.oib?'<a class="link-chip" onclick="openOIB(&quot;'+esc(d.oib)+'&quot;)">'+esc(d.oib)+'</a>':'—'}</div>
<div class="k">Datum rođenja</div><div class="v">${fmtDate(dob)}</div>
<div class="k">Mjesto rođenja</div><div class="v">${txt(d.mjesto_rodjenja||d.mjesto_rodenja)}</div>
<div class="k">Spol</div><div class="v">${txt(d.spol)}</div>
<div class="k">Visina</div><div class="v">${d.visina_cm?d.visina_cm+' cm':'—'}</div>
<div class="k">Težina</div><div class="v">${d.tezina_kg?d.tezina_kg+' kg':'—'}</div>
<div class="k">Dom. noga</div><div class="v">${txt(d.dominantna_noga)}</div>
<div class="k">Status</div><div class="v">${d.aktivan?'AKTIVAN':'NEAKTIVAN'}</div>
<div class="k">Datum pristupa</div><div class="v">${fmtDate(d.datum_pristupa)}</div>
<div class="k">Email</div><div class="v">${d.email?'<a href="mailto:'+esc(d.email)+'">'+esc(d.email)+'</a>':'—'}</div>
<div class="k">Telefon</div><div class="v">${txt(d.telefon)}</div>
<div class="k">Profil</div><div class="v">${(d.profile_url||d.scrape_url)?'<a href="'+esc(d.profile_url||d.scrape_url)+'" target="_blank">↗ vanjski profil</a>':'—'}</div>
</div>
${d.biografija ? '<div class="card" style="margin-top:14px"><div class="card-t">Biografija</div><div style="font-size:12px;line-height:1.5;color:var(--t1);margin-top:6px">'+esc(d.biografija)+'</div></div>' : ''}
</div>
<div id="p-god" class="ptab" style="display:none">
${godisnjaci.length ? `<div class="kv">
<div class="k">Prvi godišnjak</div><div class="v">${esc(d.godisnjak_prvi||godisnjaci[0])}</div>
<div class="k">Zadnji godišnjak</div><div class="v">${esc(d.godisnjak_zadnji||godisnjaci[godisnjaci.length-1])}</div>
<div class="k">Sve godine</div><div class="v">${godisnjaci.map(g=>'<span class="tag b">'+esc(g)+'</span>').join(' ')}</div>
</div>` : '<div class="empty">Nema podataka o godišnjacima</div>'}
</div>
<div id="p-nag" class="ptab" style="display:none">
${nagrade.length ? `<div style="overflow-x:auto"><table>
<thead><tr><th>Godina</th><th>Nagrada</th><th>Razina</th></tr></thead>
<tbody>${nagrade.map(n => `
<tr class="no-click">
<td>${esc(n.godina||'')}</td>
<td>${esc(n.naziv||n.nagrada||'')}</td>
<td>${esc(n.razina||'')}</td>
</tr>`).join('')}
</tbody>
</table></div>` : '<div class="empty">Nema zabilježenih nagrada</div>'}
</div>
${enrichBlock('sportas', d.id)}
`;
openPanel('Sportaš · '+(d.ime||'')+' '+(d.prezime||''), html);
}
function switchPlayerTab(el, tabId){
const parent = el.parentElement;
parent.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
el.classList.add('active');
parent.parentElement.querySelectorAll('.ptab').forEach(t=>t.style.display='none');
const target = document.getElementById(tabId);
if(target) target.style.display='block';
}
//=========== FINANCIJE ===========
async function loadFinancije(){
const root = $('#pg-financije');
root.innerHTML = `
<div class="toolbar">
<select id="fi-god">
<option value="2026">2026</option>
<option value="2025">2025</option>
<option value="2024">2024</option>
</select>
<input type="search" id="fi-q" placeholder="🔍 Pretraži korisnika ili sport…">
<div class="toggle">
<button id="fi-card" class="${_state.viewFinancije==='card'?'active':''}" onclick="setFinancijeView('card')">Kartice</button>
<button id="fi-table-btn" class="${_state.viewFinancije==='table'?'active':''}" onclick="setFinancijeView('table')">Tablica</button>
</div>
<span class="tb-s" id="fi-cnt"></span>
</div>
<div id="fi-kpi"></div>
<div class="row-2" style="margin-top:14px">
<div class="card">
<div class="card-h"><div class="card-t">📊 Raspodjela po sportovima</div></div>
<div class="chart-box"><canvas id="chSport"></canvas></div>
</div>
<div class="card" id="fi-top"></div>
</div>
<div class="card" style="margin-top:14px">
<div class="card-h"><div class="card-t">📋 Svi primatelji</div></div>
<div id="fi-table"></div>
</div>
`;
$('#fi-god').addEventListener('change', refreshFinancije);
$('#fi-q').addEventListener('input', debounce(refreshFinancije, 200));
refreshFinancije();
}
async function refreshFinancije(){
const god = $('#fi-god').value;
const q = ($('#fi-q').value || '').toLowerCase().trim();
const [analytics, byyear] = await Promise.all([
api('/v2/analytics/proracun-sport?godina='+god),
api('/v2/potpore/by-year?godina='+god)
]);
const total = (analytics && analytics.total) || (byyear && byyear.total) || 0;
const poSportu = (analytics && analytics.po_sportu) || [];
let rows = (byyear && byyear.results) || [];
if(q) rows = rows.filter(r => (r.korisnik||'').toLowerCase().includes(q) || (r.sport||'').toLowerCase().includes(q));
$('#fi-kpi').innerHTML = `
<div class="kpi-grid">
<div class="kpi"><div class="kpi-l">Ukupno ${god}</div><div class="kpi-v">${fmtEur(total)}</div></div>
<div class="kpi b"><div class="kpi-l">Primatelja</div><div class="kpi-v">${rows.length}</div></div>
<div class="kpi g"><div class="kpi-l">Sportova</div><div class="kpi-v">${poSportu.filter(x=>x.sport).length}</div></div>
<div class="kpi"><div class="kpi-l">Najveća stavka</div><div class="kpi-v" style="font-size:18px">${rows.length?fmtEur(Math.max.apply(null, rows.map(r=>Number(r.iznos_eur||0)))):'—'}</div></div>
</div>
`;
$('#fi-cnt').textContent = rows.length+' primatelja';
const top10 = rows.slice().sort((a,b)=>Number(b.iznos_eur||0)-Number(a.iznos_eur||0)).slice(0,10);
$('#fi-top').innerHTML = `
<div class="card-h"><div class="card-t">🏆 Top 10 primatelja</div></div>
<div style="overflow-x:auto;max-height:340px;overflow-y:auto"><table>
<thead><tr><th>#</th><th>Korisnik</th><th class="num">Iznos</th></tr></thead>
<tbody>${top10.map((r,i) => `
<tr class="no-click">
<td>${i+1}</td>
<td>${esc(r.korisnik)}</td>
<td class="num"><b>${fmtEur(r.iznos_eur)}</b></td>
</tr>`).join('')}</tbody>
</table></div>
`;
const sportData = poSportu.filter(x=>x.sport).slice(0, 12);
drawFinancijeChart(sportData);
let sortedRows = rows.slice();
if(_sort.financije) sortedRows = sortRows(sortedRows, _sort.financije.key, _sort.financije.dir);
if(_state.viewFinancije === 'card'){
$('#fi-table').innerHTML = '<div class="grid">'+sortedRows.map(r => `
<div class="entity" onclick='openPrimateljDetail(${JSON.stringify(r).replace(/'/g,"&#39;")})'>
${r.source_url?'<div class="et-tag">📄 PDF</div>':''}
<div class="et">${esc(r.korisnik)}</div>
<div class="es">${txt(r.sport,'—')} · ${txt(r.vrsta,'')}</div>
<div class="em">
<span><b style="color:var(--pgz-gold)">${fmtEur(r.iznos_eur)}</b></span>
<span>${esc(r.godina||'')}</span>
</div>
</div>`).join('')+'</div>';
} else {
$('#fi-table').innerHTML = `<div style="overflow-x:auto"><table>
<thead><tr><th>#</th>${sortHeader('financije','korisnik','Korisnik','')}${sortHeader('financije','sport','Sport','')}${sortHeader('financije','vrsta','Vrsta','')}${sortHeader('financije','iznos_eur','Iznos','num')}${sortHeader('financije','izvor','Izvor','')}<th>PDF</th></tr></thead>
<tbody>${sortedRows.map((r,i) => `
<tr onclick='openPrimateljDetail(${JSON.stringify(r).replace(/'/g,"&#39;")})'>
<td>${i+1}</td>
<td><b>${esc(r.korisnik)}</b></td>
<td>${txt(r.sport)}</td>
<td>${txt(r.vrsta)}</td>
<td class="num"><b>${fmtEurFull(r.iznos_eur)}</b></td>
<td>${txt(r.izvor)}</td>
<td>${r.source_url?'<a href="'+esc(r.source_url)+'" target="_blank" onclick="event.stopPropagation()">📄</a>':'—'}</td>
</tr>`).join('')}
</tbody>
</table></div>`;
}
}
function setFinancijeView(v){
_state.viewFinancije = v;
const cb = $('#fi-card'), tb = $('#fi-table-btn');
if(cb) cb.classList.toggle('active', v==='card');
if(tb) tb.classList.toggle('active', v==='table');
refreshFinancije();
}
function drawFinancijeChart(data){
const ctx = $('#chSport');
if(!ctx) return;
if(_financijeChart){ _financijeChart.destroy(); _financijeChart=null; }
const colors = ['#003087','#004CC4','#F4C430','#00e88f','#00c8e8','#ff2d55','#f59e0b','#a855f7','#ec4899','#14b8a6','#84cc16','#f97316'];
_financijeChart = new Chart(ctx, {
type:'doughnut',
data:{
labels: data.map(x=>x.sport),
datasets:[{
data: data.map(x=>x.ukupno),
backgroundColor: colors.slice(0, data.length),
borderColor: '#0d1021',
borderWidth:2
}]
},
options:{
responsive:true, maintainAspectRatio:false,
plugins:{
legend:{position:'right', labels:{color:'#e2e6f0', font:{size:10}, boxWidth:10}},
tooltip:{callbacks:{label:c => c.label+': €'+Number(c.parsed).toLocaleString('hr-HR')}}
}
}
});
}
//=========== OBJEKTI ===========
async function loadObjekti(){
const root = $('#pg-objekti');
if(!_cache.objekti){
root.innerHTML = '<div class="loading">Učitavanje objekata…</div>';
const d = await api('/sportski-objekti');
if(!d){ root.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
_cache.objekti = d.rows || (Array.isArray(d) ? d : []);
}
renderObjektiShell();
applyObjektiFilter();
}
function renderObjektiShell(){
const root = $('#pg-objekti');
const tipovi = Array.from(new Set((_cache.objekti||[]).map(o=>o.tip).filter(Boolean))).sort();
const grads = Array.from(new Set((_cache.objekti||[]).map(o=>o.grad).filter(Boolean))).sort();
root.innerHTML = `
<div class="toolbar">
<input type="search" id="ob-q" placeholder="🔍 Pretraži objekt…">
<select id="ob-tip"><option value="">Svi tipovi</option>${tipovi.map(t=>'<option value="'+esc(t)+'">'+esc(t)+'</option>').join('')}</select>
<select id="ob-grad"><option value="">Svi gradovi</option>${grads.map(g=>'<option value="'+esc(g)+'">'+esc(g)+'</option>').join('')}</select>
<label><input type="checkbox" id="ob-geo"> Samo s koordinatama</label>
<div class="toggle">
<button id="ob-card" class="${_state.viewObjekti==='card'?'active':''}" onclick="setObjektiView('card')">Kartice</button>
<button id="ob-table" class="${_state.viewObjekti==='table'?'active':''}" onclick="setObjektiView('table')">Tablica</button>
</div>
<span class="tb-s" id="ob-cnt"></span>
</div>
<div id="ob-out"></div>
`;
$('#ob-q').addEventListener('input', debounce(applyObjektiFilter, 200));
$('#ob-tip').addEventListener('change', applyObjektiFilter);
$('#ob-grad').addEventListener('change', applyObjektiFilter);
$('#ob-geo').addEventListener('change', applyObjektiFilter);
}
function setObjektiView(v){
_state.viewObjekti = v;
$('#ob-card').classList.toggle('active', v==='card');
$('#ob-table').classList.toggle('active', v==='table');
applyObjektiFilter();
}
function applyObjektiFilter(){
const q = (($('#ob-q')?$('#ob-q').value:'') || '').toLowerCase().trim();
const tip = $('#ob-tip') ? $('#ob-tip').value : '';
const grad = $('#ob-grad') ? $('#ob-grad').value : '';
const geo = $('#ob-geo') ? $('#ob-geo').checked : false;
let rows = _cache.objekti || [];
if(q) rows = rows.filter(o => (o.naziv||'').toLowerCase().includes(q) || (o.upravitelj||'').toLowerCase().includes(q));
if(tip) rows = rows.filter(o => o.tip===tip);
if(grad) rows = rows.filter(o => o.grad===grad);
if(geo) rows = rows.filter(o => o.lat && o.lng);
if(_sort.objekti) rows = sortRows(rows, _sort.objekti.key, _sort.objekti.dir);
$('#ob-cnt').textContent = rows.length+' objekata';
$('#ob-out').innerHTML = _state.viewObjekti==='card' ? renderObjektiGrid(rows) : renderObjektiTable(rows);
}
function renderObjektiGrid(rows){
if(!rows.length) return '<div class="empty">Nema rezultata</div>';
return '<div class="grid">'+rows.map(o => `
<div class="entity" onclick="openObjekt(${o.id})">
${o.lat&&o.lng?'<div class="et-tag">📍 GEO</div>':''}
<div class="et">${esc(o.naziv)}</div>
<div class="es">${txt(o.tip,'—')} · ${txt(o.grad,'—')}</div>
<div class="em">
${o.upravitelj?'<span>'+esc(o.upravitelj)+'</span>':''}
${o.kapacitet?'<span><b>'+fmtNum(o.kapacitet)+'</b> mjesta</span>':''}
${o.izgradeno?'<span>est. <b>'+esc(o.izgradeno)+'</b></span>':''}
</div>
</div>`).join('')+'</div>';
}
function renderObjektiTable(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('objekti','naziv','Naziv','')}${sortHeader('objekti','tip','Tip','')}${sortHeader('objekti','grad','Grad','')}${sortHeader('objekti','upravitelj','Upravitelj','')}${sortHeader('objekti','izgradeno','Izgrađeno','num')}${sortHeader('objekti','lat','GPS','')}</tr></thead>
<tbody>${rows.map(o => `
<tr onclick="openObjekt(${o.id})">
<td><b>${esc(o.naziv)}</b></td>
<td>${txt(o.tip)}</td>
<td>${txt(o.grad)}</td>
<td>${txt(o.upravitelj)}</td>
<td class="num">${txt(o.izgradeno)}</td>
<td>${o.lat&&o.lng?'<span class="tag gr">📍</span>':'—'}</td>
</tr>`).join('')}</tbody>
</table></div>`;
}
function openObjekt(id){
const o = (_cache.objekti||[]).find(x => x.id===id);
if(!o){ openPanel('Objekt', '<div class="empty">Objekt nije pronađen</div>'); return; }
const mapUrl = (o.lat && o.lng) ? 'https://www.google.com/maps/search/?api=1&query='+o.lat+','+o.lng : null;
const embedUrl = (o.lat && o.lng) ? 'https://www.google.com/maps?q='+encodeURIComponent((o.naziv||'')+', '+(o.grad||'')+', PGŽ')+'&ll='+o.lat+','+o.lng+'&z=17&output=embed' : null;
const html = `
<div class="card-h" style="border:0;padding:0;margin-bottom:14px">
<div>
<div style="font-size:18px;font-weight:800;color:var(--t0)">${esc(o.naziv)}</div>
<div style="font-size:12px;color:var(--t2);margin-top:4px">${txt(o.tip,'—')} · ${txt(o.grad,'')}</div>
</div>
</div>
${embedUrl?'<iframe class="iframe-map" style="height:240px" src="'+embedUrl+'" loading="lazy"></iframe>':''}
<div class="card" style="margin-top:14px">
<div class="card-h"><div class="card-t">📋 Detalji</div></div>
<div class="kv">
<div class="k">Tip</div><div class="v">${txt(o.tip)}</div>
<div class="k">Adresa</div><div class="v">${txt(o.adresa)}</div>
<div class="k">Grad</div><div class="v">${txt(o.grad)}</div>
<div class="k">Upravitelj</div><div class="v">${txt(o.upravitelj)}</div>
<div class="k">OIB</div><div class="v">${txt(o.upravitelj_oib)}</div>
<div class="k">Kapacitet</div><div class="v">${o.kapacitet?fmtNum(o.kapacitet)+' mjesta':'—'}</div>
<div class="k">Veličina</div><div class="v">${txt(o.veličina)}</div>
<div class="k">Sportovi</div><div class="v">${(o.sportovi||[]).map(s=>'<span class="tag b">'+esc(s)+'</span>').join(' ')||'—'}</div>
<div class="k">Izgrađeno</div><div class="v">${txt(o.izgradeno)}</div>
<div class="k">Obnovljeno</div><div class="v">${txt(o.obnovljeno_god)}</div>
<div class="k">Natkriven</div><div class="v">${o.natkrita?'DA':'NE'}</div>
<div class="k">Web</div><div class="v">${o.web?'<a href="'+esc(o.web)+'" target="_blank">'+esc(o.web)+'</a>':'—'}</div>
<div class="k">Koordinate</div><div class="v">${(o.lat&&o.lng)?'<a href="'+mapUrl+'" target="_blank">'+o.lat.toFixed(5)+', '+o.lng.toFixed(5)+' ↗</a>':'—'}</div>
</div>
</div>
`;
openPanel('Objekt · '+o.naziv, html);
}
//=========== MANIFESTACIJE ===========
async function loadManifestacije(){
const root = $('#pg-manifestacije');
if(!_cache.manifestacije){
root.innerHTML = '<div class="loading">Učitavanje manifestacija…</div>';
const d = await api('/manifestacije-full');
if(!d){ root.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
_cache.manifestacije = d.rows || (Array.isArray(d) ? d : []);
}
renderManifShell();
applyManifFilter();
}
function renderManifShell(){
const root = $('#pg-manifestacije');
const razine = Array.from(new Set((_cache.manifestacije||[]).map(m=>m.razina).filter(Boolean))).sort();
root.innerHTML = `
<div class="toolbar">
<input type="search" id="mn-q" placeholder="🔍 Pretraži manifestaciju…">
<select id="mn-raz"><option value="">Sve razine</option>${razine.map(r=>'<option value="'+esc(r)+'">'+esc(r)+'</option>').join('')}</select>
<div class="toggle">
<button id="mn-card" class="${_state.viewManif==='card'?'active':''}" onclick="setManifView('card')">Kartice</button>
<button id="mn-table" class="${_state.viewManif==='table'?'active':''}" onclick="setManifView('table')">Tablica</button>
</div>
<span class="tb-s" id="mn-cnt"></span>
</div>
<div id="mn-out"></div>
`;
$('#mn-q').addEventListener('input', debounce(applyManifFilter, 200));
$('#mn-raz').addEventListener('change', applyManifFilter);
}
function setManifView(v){
_state.viewManif = v;
$('#mn-card').classList.toggle('active', v==='card');
$('#mn-table').classList.toggle('active', v==='table');
applyManifFilter();
}
function applyManifFilter(){
const q = (($('#mn-q')?$('#mn-q').value:'') || '').toLowerCase().trim();
const raz = $('#mn-raz') ? $('#mn-raz').value : '';
let rows = _cache.manifestacije || [];
if(q) rows = rows.filter(m => (m.naziv||'').toLowerCase().includes(q) || (m.organizator||'').toLowerCase().includes(q) || (m.mjesto||'').toLowerCase().includes(q));
if(raz) rows = rows.filter(m => m.razina===raz);
if(_sort.manifestacije) rows = sortRows(rows, _sort.manifestacije.key, _sort.manifestacije.dir);
$('#mn-cnt').textContent = rows.length+' manifestacija';
$('#mn-out').innerHTML = _state.viewManif==='card' ? renderManifGrid(rows) : renderManifTable(rows);
}
function renderManifGrid(rows){
if(!rows.length) return '<div class="empty">Nema rezultata</div>';
return '<div class="grid">'+rows.map(m => `
<div class="entity" onclick="openManif(${m.id})">
${m.razina?'<div class="et-tag">'+esc(m.razina)+'</div>':''}
<div class="et">${esc(m.naziv)}</div>
<div class="es">${txt(m.mjesto,'—')}${m.spol_kategorija?' · '+esc(m.spol_kategorija):''}</div>
<div class="em">
${m.broj_ucesnika?'<span><b>'+esc(m.broj_ucesnika)+'</b> sudionika</span>':''}
${m.organizator?'<span>'+esc((m.organizator||'').slice(0,40))+'</span>':''}
</div>
</div>`).join('')+'</div>';
}
function renderManifTable(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('manifestacije','naziv','Naziv','')}${sortHeader('manifestacije','mjesto','Mjesto','')}${sortHeader('manifestacije','razina','Razina','')}${sortHeader('manifestacije','organizator','Organizator','')}${sortHeader('manifestacije','broj_ucesnika','Sudionici','')}<th>Link</th></tr></thead>
<tbody>${rows.map(m => `
<tr onclick="openManif(${m.id})">
<td><b>${esc(m.naziv)}</b></td>
<td>${txt(m.mjesto)}</td>
<td>${m.razina?'<span class="tag b">'+esc(m.razina)+'</span>':'—'}</td>
<td>${txt(m.organizator)}</td>
<td>${txt(m.broj_ucesnika)}</td>
<td>${m.source_url?'<a href="'+esc(m.source_url)+'" target="_blank">↗</a>':'—'}</td>
</tr>`).join('')}</tbody>
</table></div>`;
}
function openManif(id){
const m = (_cache.manifestacije||[]).find(x => x.id===id);
if(!m){ openPanel('Manifestacija', '<div class="empty">Nije pronađeno</div>'); return; }
// If we have a source_url, open it directly in a new tab
if(m.source_url){
window.open(m.source_url, '_blank', 'noopener');
return;
}
// Otherwise show details + Google search fallback
const gq = encodeURIComponent((m.naziv||'')+' '+(m.mjesto||'')+' sport');
const googleUrl = 'https://www.google.com/search?q='+gq;
const html = `
<div class="card-h" style="border:0;padding:0;margin-bottom:14px">
<div>
<div style="font-size:18px;font-weight:800;color:var(--t0)">${esc(m.naziv)}</div>
<div style="font-size:12px;color:var(--t2);margin-top:4px">${txt(m.mjesto,'—')} · ${txt(m.razina,'')}</div>
</div>
</div>
<div class="card">
<div class="card-h"><div class="card-t">📋 Detalji</div></div>
<div class="kv">
<div class="k">Organizator</div><div class="v">${txt(m.organizator)}</div>
<div class="k">Razina</div><div class="v">${m.razina?'<span class="tag b">'+esc(m.razina)+'</span>':'—'}</div>
<div class="k">Sudionici</div><div class="v">${txt(m.broj_ucesnika)}</div>
<div class="k">Spol/kategorija</div><div class="v">${txt(m.spol_kategorija)}</div>
<div class="k">Godina od</div><div class="v">${txt(m.godina_od)}</div>
<div class="k">Mjesto</div><div class="v">${txt(m.mjesto)}</div>
</div>
${m.napomena ? '<div style="margin-top:14px;font-size:12px;line-height:1.5;color:var(--t1);padding:10px;background:var(--bg3);border-radius:5px">'+esc(m.napomena)+'</div>' : ''}
</div>
<div class="card">
<div class="card-h"><div class="card-t">🌐 Online izvori</div></div>
<div class="empty" style="padding:14px">Nema poznatog izvornog URL-a. Pokušaj pronaći više informacija online:</div>
<div style="text-align:center;margin-top:10px">
<a href="${googleUrl}" target="_blank" class="btn primary" style="display:inline-block;text-decoration:none">🔍 Pretraži na Googleu</a>
</div>
</div>
`;
openPanel('Manifestacija · '+m.naziv, html);
}
//=========== MREŽA (Network Graph) ===========
const _mreza = {data:null, sim:null, allNodes:null, allEdges:null, filter:{osoba:'', klub:'', tvrtka:'', tip:''}};
async function loadMreza(){
const root = $('#pg-mreza');
if(!_mreza.data){
root.innerHTML = '<div class="loading">Učitavanje grafa entiteta…</div>';
let resp;
try{
const r = await fetch('https://api.rinet.one/api/v1/presenter/graph-real');
resp = await r.json();
}catch(e){ console.error('graph fetch error', e); }
if(!resp || !resp.data){ root.innerHTML='<div class="empty">Greška pri dohvatu graf-podataka</div>'; return; }
_mreza.data = resp.data;
_mreza.allNodes = (resp.data.nodes||[]).slice();
_mreza.allEdges = (resp.data.edges||[]).slice();
}
renderMrezaShell();
renderMrezaGraph();
}
function renderMrezaShell(){
const root = $('#pg-mreza');
const types = Array.from(new Set((_mreza.allNodes||[]).map(n=>n.type))).sort();
const totalN = (_mreza.allNodes||[]).length;
const totalE = (_mreza.allEdges||[]).length;
root.innerHTML = `
<div class="kpi-grid" style="grid-template-columns:repeat(4,1fr);margin-bottom:14px">
<div class="kpi"><div class="kpi-l">Čvorova</div><div class="kpi-v">${totalN}</div></div>
<div class="kpi b"><div class="kpi-l">Veza</div><div class="kpi-v">${totalE}</div></div>
<div class="kpi g"><div class="kpi-l">Osoba</div><div class="kpi-v">${(_mreza.allNodes||[]).filter(n=>n.type==='person').length}</div></div>
<div class="kpi r"><div class="kpi-l">Tvrtki / entiteta</div><div class="kpi-v">${(_mreza.allNodes||[]).filter(n=>n.type==='entity'||n.type==='supplier').length}</div></div>
</div>
<div class="toolbar" style="margin-bottom:10px">
<input type="search" id="mr-osoba" placeholder="👤 Osoba…">
<input type="search" id="mr-klub" placeholder="🏟 Klub / Savez…">
<input type="search" id="mr-tvrtka" placeholder="🏢 Tvrtka / Entitet…">
<select id="mr-tip">
<option value="">Svi tipovi</option>
${types.map(t=>'<option value="'+esc(t)+'">'+esc(t)+'</option>').join('')}
</select>
<button class="btn" onclick="resetMreza()">↺ Reset</button>
<span class="tb-s" id="mr-cnt"></span>
</div>
<div class="card" style="padding:0;overflow:hidden;position:relative">
<div id="mr-graph" style="width:100%;height:640px;background:#08090e;position:relative;cursor:grab"></div>
<div style="position:absolute;top:10px;right:14px;font-size:10px;color:var(--t4);background:rgba(13,16,33,0.7);padding:4px 8px;border-radius:4px;pointer-events:none">
🖱 Drag • Scroll zoom • Right-drag pan • Click node
</div>
</div>
<div class="card" style="margin-top:10px">
<div class="card-h"><div class="card-t">🎨 Legenda</div></div>
<div style="display:flex;gap:14px;flex-wrap:wrap;font-size:12px">
<div><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#8b5cf6;vertical-align:middle;margin-right:5px"></span>Osoba</div>
<div><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#ff4466;vertical-align:middle;margin-right:5px"></span>Entitet (high risk)</div>
<div><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#00e68a;vertical-align:middle;margin-right:5px"></span>Dobavljač</div>
<div style="color:var(--t2)">Veličina = risk / promet · Klikni čvor za detalje · 3D force graph (drag rotate, scroll zoom)</div>
</div>
</div>
`;
$('#mr-osoba').addEventListener('input', debounce(applyMrezaFilter, 200));
$('#mr-klub').addEventListener('input', debounce(applyMrezaFilter, 200));
$('#mr-tvrtka').addEventListener('input', debounce(applyMrezaFilter, 200));
$('#mr-tip').addEventListener('change', applyMrezaFilter);
}
function applyMrezaFilter(){
const osoba = ($('#mr-osoba').value||'').toLowerCase().trim();
const klub = ($('#mr-klub').value||'').toLowerCase().trim();
const tvrtka = ($('#mr-tvrtka').value||'').toLowerCase().trim();
const tip = $('#mr-tip').value;
let nodes = (_mreza.allNodes||[]).slice();
if(osoba) nodes = nodes.filter(n => n.type==='person' && (n.label||'').toLowerCase().includes(osoba) || n.type!=='person');
if(klub){
// filter entity/supplier by label match (savezi/klubovi appear as entities)
nodes = nodes.filter(n => {
if(n.type==='person') return true;
return (n.label||'').toLowerCase().includes(klub);
});
}
if(tvrtka){
nodes = nodes.filter(n => {
if(n.type==='person') return true;
return (n.label||'').toLowerCase().includes(tvrtka);
});
}
if(tip) nodes = nodes.filter(n => n.type===tip);
// Also filter to stronger: if osoba is set, drop persons not matching
if(osoba) nodes = nodes.filter(n => n.type!=='person' || (n.label||'').toLowerCase().includes(osoba));
if(klub) nodes = nodes.filter(n => n.type==='person' || (n.label||'').toLowerCase().includes(klub));
if(tvrtka) nodes = nodes.filter(n => n.type==='person' || (n.label||'').toLowerCase().includes(tvrtka));
const ids = new Set(nodes.map(n=>n.id));
let edges = (_mreza.allEdges||[]).filter(e => ids.has(e.source.id||e.source) && ids.has(e.target.id||e.target));
// After edge filter, keep only nodes that have at least one edge OR were direct matches
const used = new Set();
for(const e of edges){
used.add(e.source.id||e.source);
used.add(e.target.id||e.target);
}
// If we have any text filter, restrict to nodes that have edges; otherwise keep all
if(osoba||klub||tvrtka){
nodes = nodes.filter(n => used.has(n.id));
}
$('#mr-cnt').textContent = nodes.length+' čvorova · '+edges.length+' veza';
renderMrezaGraph(nodes, edges);
}
function resetMreza(){
$('#mr-osoba').value = '';
$('#mr-klub').value = '';
$('#mr-tvrtka').value = '';
$('#mr-tip').value = '';
applyMrezaFilter();
}
function renderMrezaGraph(nodes, edges){
if(!nodes) nodes = (_mreza.allNodes||[]).slice();
if(!edges) edges = (_mreza.allEdges||[]).slice();
const container = document.getElementById('mr-graph');
if(!container) return;
// Deep-copy so 3d-force-graph doesn't mutate originals across re-renders
const N = nodes.map(n => Object.assign({}, n));
const Nmap = new Map(N.map(n=>[n.id, n]));
const E = edges.map(e => ({
source: e.source.id || e.source,
target: e.target.id || e.target,
color: e.color, size: e.size
})).filter(e => Nmap.has(e.source) && Nmap.has(e.target));
// Tear down previous graph (re-create on each render to avoid stale state)
if(_mreza.graph){
try{ _mreza.graph._destructor && _mreza.graph._destructor(); }catch(e){}
container.innerHTML = '';
_mreza.graph = null;
}
if(typeof ForceGraph3D === 'undefined'){
container.innerHTML = '<div class="empty" style="padding:40px;color:var(--red)">3D Force Graph biblioteka nije učitana. Provjeri unpkg.com pristup.</div>';
return;
}
const W = container.clientWidth || 800;
const H = container.clientHeight || 640;
const Graph = ForceGraph3D()(container)
.width(W)
.height(H)
.backgroundColor('#08090e')
.graphData({nodes: N, links: E})
.nodeLabel(n => '<div style="background:rgba(13,16,33,.95);border:1px solid #283560;border-radius:5px;padding:6px 10px;font-family:Inter,sans-serif;font-size:12px;color:#fff"><b>'+(n.label||'').replace(/</g,'&lt;')+'</b><br><span style="color:#8a95b4">'+(n.type||'')+(n.meta&&n.meta.risk?' · risk '+n.meta.risk:'')+'</span></div>')
.nodeColor(n => n.color || '#004CC4')
.nodeVal(n => Math.max(2, (n.size||5)*0.6))
.nodeOpacity(0.92)
.linkColor(l => (l.color||'#283560').replace(/22$/,'') )
.linkWidth(l => Math.max(0.3, (l.size||0.4)*1.5))
.linkOpacity(0.5)
.linkDirectionalParticles(0)
.onNodeClick(n => {
// Center camera on node + open detail panel
const dist = 80;
const distRatio = 1 + dist/Math.hypot(n.x||1, n.y||1, n.z||1);
Graph.cameraPosition(
{ x:(n.x||0)*distRatio, y:(n.y||0)*distRatio, z:(n.z||0)*distRatio },
n,
800
);
openMrezaNode(n);
})
.onNodeHover(n => { container.style.cursor = n ? 'pointer' : 'grab'; });
_mreza.graph = Graph;
// Resize handler — re-flow when container dims change
if(_mreza.resizeObs){ try{_mreza.resizeObs.disconnect();}catch(e){} }
_mreza.resizeObs = new ResizeObserver(entries => {
for(const ent of entries){
const cw = ent.contentRect.width|0, ch = ent.contentRect.height|0;
if(cw>0 && ch>0 && _mreza.graph){
try{ _mreza.graph.width(cw).height(ch); }catch(e){}
}
}
});
_mreza.resizeObs.observe(container);
if($('#mr-cnt') && !$('#mr-cnt').textContent){
$('#mr-cnt').textContent = N.length+' čvorova · '+E.length+' veza';
}
}
function openMrezaNode(n){
const m = n.meta || {};
// Find connected nodes
const id = n.id;
const edges = (_mreza.allEdges||[]).filter(e => (e.source.id||e.source)===id || (e.target.id||e.target)===id);
const connectedIds = new Set();
for(const e of edges){
const s = e.source.id||e.source;
const t = e.target.id||e.target;
if(s===id) connectedIds.add(t); else connectedIds.add(s);
}
const connected = (_mreza.allNodes||[]).filter(x => connectedIds.has(x.id));
let html = `
<div class="card-h" style="border:0;padding:0;margin-bottom:14px">
<div>
<div style="font-size:18px;font-weight:800;color:var(--t0)">${esc(n.label)}</div>
<div style="font-size:12px;color:var(--t2);margin-top:4px">
<span class="tag b">${esc(n.type)}</span>
${m.risk?'<span class="tag rd">Risk '+m.risk+'</span>':''}
${m.forensic?'<span class="tag am">Forenzika '+m.forensic+'</span>':''}
</div>
</div>
</div>
${(m.risk!=null || m.forensic!=null) ? `
<div class="kpi-grid" style="grid-template-columns:repeat(3,1fr);margin-bottom:14px">
${m.risk!=null ? '<div class="kpi r"><div class="kpi-l">Risk score</div><div class="kpi-v">'+m.risk+'</div></div>' : ''}
${m.forensic!=null ? '<div class="kpi"><div class="kpi-l">Forenzički flag</div><div class="kpi-v">'+m.forensic+'</div></div>' : ''}
${m.winner_contracts!=null ? '<div class="kpi b"><div class="kpi-l">Ugovori (W)</div><div class="kpi-v">'+m.winner_contracts+'</div></div>' : ''}
</div>` : ''}
<div class="card">
<div class="card-h"><div class="card-t">📋 Detalji</div></div>
<div class="kv">
<div class="k">ID</div><div class="v" style="font-family:var(--mono);font-size:11px">${esc(n.id)}</div>
<div class="k">Tip</div><div class="v">${esc(n.type)}</div>
<div class="k">Naziv</div><div class="v">${esc(n.label)}</div>
${m.oib?'<div class="k">OIB</div><div class="v" style="font-family:var(--mono)">'+esc(m.oib)+'</div>':''}
${m.city?'<div class="k">Grad</div><div class="v">'+esc(m.city)+'</div>':''}
${m.buyer_contracts!=null?'<div class="k">Ugovori kao kupac</div><div class="v">'+m.buyer_contracts+'</div>':''}
${m.buyer_value!=null?'<div class="k">Vrijednost (kupac)</div><div class="v">'+fmtEurFull(m.buyer_value)+'</div>':''}
${m.winner_contracts!=null?'<div class="k">Ugovori kao dobavljač</div><div class="v">'+m.winner_contracts+'</div>':''}
${m.total!=null?'<div class="k">Ukupan promet</div><div class="v"><b style="color:var(--pgz-gold)">'+fmtEurFull(m.total)+'</b></div>':''}
${m.contracts!=null?'<div class="k">Broj ugovora</div><div class="v">'+m.contracts+'</div>':''}
</div>
</div>
<div class="card">
<div class="card-h"><div class="card-t">🔗 Veze (${connected.length})</div></div>
${connected.length ? '<div style="overflow-x:auto;max-height:300px;overflow-y:auto"><table>'+
'<thead><tr><th>Tip</th><th>Naziv</th><th>Risk</th></tr></thead>'+
'<tbody>'+connected.slice(0,80).map(c => `
<tr onclick="openMrezaNode(${JSON.stringify(c).replace(/"/g,'&quot;')})">
<td><span class="tag b">${esc(c.type)}</span></td>
<td><b>${esc(c.label)}</b></td>
<td>${(c.meta&&c.meta.risk)||'—'}</td>
</tr>`).join('')+
'</tbody></table></div>' : '<div class="empty">Nema povezanih entiteta</div>'}
</div>
${(m.forensic && m.forensic > 0 && n.type==='entity') ? `
<div class="card">
<div class="card-h"><div class="card-t">⚠ Forenzika</div></div>
<div class="alert-card crit">
<div class="at">${m.forensic} forenzičkih flagova</div>
<div class="ad">Ovaj entitet ima zabilježene forenzičke nalaze. Provjeri detalje u sekciji Forenzika.</div>
</div>
</div>` : ''}
${(m.buyer_contracts && m.buyer_contracts > 0) ? `
<div class="card">
<div class="card-h"><div class="card-t">💼 Procurement</div></div>
<div class="kv">
<div class="k">Kao kupac</div><div class="v">${m.buyer_contracts} ugovora · ${fmtEurFull(m.buyer_value||0)}</div>
</div>
</div>` : ''}
`;
openPanel(n.label, html);
}
//=========== FORENZIKA ===========
const _forenzika = {alerts:null, custom:null, filter:{severity:'', tip:'', q:''}};
// Manual / custom forensic findings (not in DB) — flagship cases
const _customFindings = [
{
id: 'liveric-pep',
severity: 'CRITICAL',
tip: 'sukob_interesa',
naslov: 'Velimir Liverić — PEP profil',
sazetak: 'Politički eksponirana osoba s povezanostima u sportskom ekosustavu PGŽ. Manualno označen za pojačanu reviziju.',
osoba: 'Velimir Liverić',
uloga: 'Politički eksponirana osoba (PEP)',
risk_score: 87,
uvod: 'Velimir Liverić figurira u više dokumenata vezanih uz raspodjelu sportskih sredstava u PGŽ. Zbog statusa politički eksponirane osobe (PEP) prema GDPR/AML standardima, sve transakcije i pozicije u upravnim odborima trebaju biti predmet pojačane provjere (EDD).',
povezane_entitete: [
{tip:'Tvrtka', naziv:'Politička pozicija u JLS', napomena:'Aktualna ili bivša funkcija u jedinici lokalne/područne samouprave'},
{tip:'Klub', naziv:'Provjeriti članstva u upravnim tijelima', napomena:'Manualna provjera potrebna'},
{tip:'Potpora', naziv:'Sufinanciranja u 20242026', napomena:'Provjeriti tokove novca prema povezanim klubovima'}
],
timeline: [
{kad:'2026-01', sto:'Prvi flag — automatska detekcija imena u dokumentima'},
{kad:'2026-03', sto:'Ručna eskalacija za EDD pregled'},
{kad:'2026-04', sto:'Dodano u registar PEP osoba pgz-sport platforme'}
],
breakdown: [
{kriterij:'PEP status', tezina:40},
{kriterij:'Više aktivnih veza s klubovima', tezina:25},
{kriterij:'Pristup javnim sredstvima', tezina:15},
{kriterij:'Nedostatak transparentne dokumentacije', tezina:7}
]
},
{
id: 'klub-bez-oib',
severity: 'HIGH',
tip: 'data_quality',
naslov: 'Klubovi bez OIB-a',
sazetak: 'Više aktivnih klubova nema upisan OIB što onemogućuje pravnu validaciju primatelja potpora.',
risk_score: 60,
uvod: 'Bez OIB-a klubovi ne mogu biti automatski povezani s registrom poreznih obveznika i Ministarstvom pravosuđa. Riziku su izložena sufinanciranja jer nije moguće potvrditi pravnu osobnost primatelja.',
povezane_entitete: [],
timeline: [{kad:'kontinuirano', sto:'Detekcija pri unosu i scrape ciklusu'}],
breakdown: [
{kriterij:'Nedostatak ključnog identifikatora', tezina:35},
{kriterij:'Više klubova pogođeno', tezina:25}
]
},
{
id: 'premali-klub-velik-iznos',
severity: 'HIGH',
tip: 'neobicna_isplata',
naslov: 'Mali klub — neproporcionalno velika potpora',
sazetak: 'Heuristička detekcija: klubovi s ispod 50 registriranih sportaša koji su dobili potporu iznad 30k EUR.',
risk_score: 55,
uvod: 'Pravilo: ako klub ima <50 sportaša a primio je >30 000 EUR potpore, vjerojatno postoji opravdani razlog (npr. infrastrukturna potpora ili nositelj kvalitete) — ali svaki slučaj zahtijeva ručnu validaciju.',
povezane_entitete: [{tip:'Klub', naziv:'Više klubova zadovoljava heuristiku'}],
timeline: [{kad:'svakodnevno', sto:'Heuristika prolazi kroz tablicu sufinanciranja'}],
breakdown: [
{kriterij:'Disproporcija veličina/iznos', tezina:30},
{kriterij:'Nepostojanje "nositelj kvalitete" oznake', tezina:25}
]
}
];
async function loadForenzika(){
const root = $('#pg-forenzika');
if(!_forenzika.alerts){
root.innerHTML = '<div class="loading">Učitavanje alarma…</div>';
const al = await api('/v2/alerts');
_forenzika.alerts = Array.isArray(al) ? al : [];
}
if(!_forenzika.custom) _forenzika.custom = _customFindings.slice();
let d = _cache.dash;
if(!d){ d = await api('/dashboard'); if(d) _cache.dash = d; }
d = d || {};
renderForenzikaShell(d);
applyForenzikaFilter();
}
function renderForenzikaShell(d){
const root = $('#pg-forenzika');
const alerts = _forenzika.alerts || [];
const tipovi = Array.from(new Set([
..._forenzika.custom.map(c=>c.tip),
...alerts.map(a=>a.tip).filter(Boolean)
])).sort();
root.innerHTML = `
<div class="kpi-grid">
<div class="kpi r"><div class="kpi-l">Kritičnih</div><div class="kpi-v">${alerts.filter(a=>a.razina==='CRITICAL').length + _forenzika.custom.filter(c=>c.severity==='CRITICAL').length}</div><div class="kpi-s">SEVERITY: CRITICAL</div></div>
<div class="kpi"><div class="kpi-l">High</div><div class="kpi-v">${_forenzika.custom.filter(c=>c.severity==='HIGH').length}</div><div class="kpi-s">forenzički nalazi</div></div>
<div class="kpi b"><div class="kpi-l">Upozorenja</div><div class="kpi-v">${alerts.filter(a=>a.razina==='WARNING').length}</div></div>
<div class="kpi g"><div class="kpi-l">Naplaćeno članarina</div><div class="kpi-v" style="font-size:18px">${fmtEur(d.naplaceno_clanarine_god||0)}</div></div>
<div class="kpi"><div class="kpi-l">Dug članarina</div><div class="kpi-v" style="font-size:18px">${fmtEur(d.dug_clanarine_god||0)}</div></div>
</div>
<div class="toolbar">
<input type="search" id="fz-q" placeholder="🔍 Pretraži po imenu, klubu, nalazu…">
<select id="fz-sev">
<option value="">Sve razine</option>
<option value="CRITICAL">🔴 CRITICAL</option>
<option value="HIGH">🟠 HIGH</option>
<option value="WARNING">🟡 WARNING</option>
<option value="MEDIUM">🟡 MEDIUM</option>
</select>
<select id="fz-tip">
<option value="">Svi tipovi</option>
${tipovi.map(t=>'<option value="'+esc(t)+'">'+esc(t)+'</option>').join('')}
</select>
<span class="tb-s" id="fz-cnt"></span>
</div>
<div class="card" style="border-color:var(--pgz-gold)">
<div class="card-h">
<div class="card-t">⚡ Pokreni novu analizu osobe</div>
<div class="tb-s">civic.persons + entity_links + forensic_findings</div>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<input type="text" id="fz-scan-name" placeholder="Ime i prezime (npr. Velimir Liverić)" style="background:var(--bg2);border:1px solid var(--rim);border-radius:5px;padding:8px 12px;color:var(--t1);font-size:13px;flex:1;min-width:240px" value="Velimir Liverić">
<button class="btn primary" onclick="runForensicScan()">▶ Pokreni</button>
<button class="btn" onclick="document.getElementById('fz-scan-out').innerHTML=''">Očisti</button>
</div>
<div id="fz-scan-out" style="margin-top:12px"></div>
</div>
<div id="fz-out"></div>
`;
$('#fz-q').addEventListener('input', debounce(applyForenzikaFilter, 200));
$('#fz-sev').addEventListener('change', applyForenzikaFilter);
$('#fz-tip').addEventListener('change', applyForenzikaFilter);
}
function applyForenzikaFilter(){
const q = (($('#fz-q')?$('#fz-q').value:'') || '').toLowerCase().trim();
const sev = $('#fz-sev') ? $('#fz-sev').value : '';
const tip = $('#fz-tip') ? $('#fz-tip').value : '';
// Combine custom findings + DB alerts into a unified list
const combined = [];
for(const c of (_forenzika.custom||[])){
combined.push({
_kind: 'custom', id: c.id, severity: c.severity, tip: c.tip,
naslov: c.naslov, poruka: c.sazetak, klub_id: null, clan_id: null, datum: null,
raw: c
});
}
for(const a of (_forenzika.alerts||[])){
combined.push({
_kind: 'alert', id: a.id, severity: a.razina, tip: a.tip,
naslov: '['+a.tip+'] '+(a.poruka||'').slice(0,80),
poruka: a.poruka, klub_id: a.klub_id, clan_id: a.clan_id, datum: a.datum,
raw: a
});
}
let rows = combined;
if(q) rows = rows.filter(r => (r.naslov||'').toLowerCase().includes(q) || (r.poruka||'').toLowerCase().includes(q) || (r.tip||'').toLowerCase().includes(q));
if(sev) rows = rows.filter(r => r.severity===sev);
if(tip) rows = rows.filter(r => r.tip===tip);
$('#fz-cnt').textContent = rows.length+' nalaza';
const out = $('#fz-out');
if(!rows.length){ out.innerHTML='<div class="empty">Nema rezultata pri tim filtrima</div>'; return; }
out.innerHTML = rows.map((r,i) => {
const sevClass = r.severity==='CRITICAL'?'crit':(r.severity==='HIGH'?'crit':'');
const sevColor = r.severity==='CRITICAL'?'rd':(r.severity==='HIGH'?'am':'b');
return `
<div class="alert-card ${sevClass}" style="cursor:pointer;display:flex;align-items:center;gap:12px;padding:12px 14px" onclick="openForensicDetail('${r._kind}','${r.id}')">
<div style="font-size:22px;flex-shrink:0">${r.severity==='CRITICAL'?'🔴':r.severity==='HIGH'?'🟠':'🟡'}</div>
<div style="flex:1;min-width:0">
<div class="at">${esc(r.naslov)}</div>
<div class="ad">${esc(r.poruka||'')}</div>
${r.datum?'<div class="ad">📅 '+fmtDate(r.datum)+'</div>':''}
</div>
<div><span class="tag ${sevColor}">${esc(r.severity||'?')}</span></div>
<div style="color:var(--t4);font-size:18px"></div>
</div>
`;
}).join('');
}
function openForensicDetail(kind, id){
if(kind === 'custom'){
const c = (_forenzika.custom||[]).find(x => x.id === id);
if(!c){ openPanel('Nalaz', '<div class="empty">Nalaz nije pronađen</div>'); return; }
return renderCustomFindingPanel(c);
} else {
const a = (_forenzika.alerts||[]).find(x => String(x.id) === String(id));
if(!a){ openPanel('Alarm', '<div class="empty">Alarm nije pronađen</div>'); return; }
return renderAlertPanel(a);
}
}
function renderCustomFindingPanel(c){
const sevColor = c.severity==='CRITICAL'?'rd':(c.severity==='HIGH'?'am':'b');
const html = `
<div class="card-h" style="border:0;padding:0;margin-bottom:14px">
<div>
<div style="font-size:18px;font-weight:800;color:var(--t0)">${esc(c.naslov)}</div>
<div style="font-size:12px;color:var(--t2);margin-top:4px">
<span class="tag ${sevColor}">${esc(c.severity)}</span>
<span class="tag">${esc(c.tip)}</span>
${c.osoba?'<span class="tag b">'+esc(c.osoba)+'</span>':''}
</div>
</div>
</div>
<div class="kpi-grid" style="grid-template-columns:1fr 1fr 1fr;margin-bottom:14px">
<div class="kpi r"><div class="kpi-l">Risk Score</div><div class="kpi-v">${c.risk_score||0}<span style="font-size:14px;color:var(--t2)">/100</span></div></div>
<div class="kpi"><div class="kpi-l">Severity</div><div class="kpi-v" style="font-size:16px">${esc(c.severity)}</div></div>
<div class="kpi b"><div class="kpi-l">Tip</div><div class="kpi-v" style="font-size:14px">${esc(c.tip)}</div></div>
</div>
<div class="card">
<div class="card-h"><div class="card-t">📖 Uvod u nalaz</div></div>
<div style="font-size:12px;line-height:1.6;color:var(--t1)">${esc(c.uvod)}</div>
</div>
${c.povezane_entitete && c.povezane_entitete.length ? `
<div class="card">
<div class="card-h"><div class="card-t">🔗 Povezani entiteti (${c.povezane_entitete.length})</div></div>
<table>
<thead><tr><th>Tip</th><th>Naziv</th><th>Napomena</th></tr></thead>
<tbody>${c.povezane_entitete.map(e => `
<tr class="no-click">
<td><span class="tag b">${esc(e.tip)}</span></td>
<td><b>${esc(e.naziv)}</b></td>
<td>${esc(e.napomena||'')}</td>
</tr>`).join('')}</tbody>
</table>
</div>` : ''}
${c.timeline && c.timeline.length ? `
<div class="card">
<div class="card-h"><div class="card-t">📅 Vremenska linija</div></div>
<div style="display:flex;flex-direction:column;gap:8px">
${c.timeline.map(t => `
<div style="display:flex;gap:10px;padding:8px 10px;background:var(--bg3);border-radius:5px;border-left:3px solid var(--pgz-blue2)">
<div style="font-family:var(--mono);color:var(--pgz-gold);font-weight:700;font-size:11px;min-width:90px">${esc(t.kad)}</div>
<div style="font-size:12px;color:var(--t1)">${esc(t.sto)}</div>
</div>
`).join('')}
</div>
</div>` : ''}
${c.breakdown && c.breakdown.length ? `
<div class="card">
<div class="card-h"><div class="card-t">📊 Risk score breakdown</div></div>
<div style="display:flex;flex-direction:column;gap:8px">
${c.breakdown.map(b => {
const pct = Math.min(100, Math.round((b.tezina/100)*100));
return `
<div>
<div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:3px">
<span style="color:var(--t1)">${esc(b.kriterij)}</span>
<span style="font-family:var(--mono);color:var(--pgz-gold);font-weight:700">+${b.tezina}</span>
</div>
<div style="height:6px;background:var(--bg3);border-radius:3px;overflow:hidden">
<div style="height:100%;background:linear-gradient(90deg,var(--red),var(--pgz-gold));width:${pct}%"></div>
</div>
</div>`;
}).join('')}
</div>
</div>` : ''}
<div class="card">
<div class="card-h"><div class="card-t">📄 Dokumenti / dokazi</div></div>
<div class="empty" style="padding:14px">Za ovaj manualni nalaz nisu priloženi PDF dokazi. Pokreni "Obogati podatke" za prikupljanje izvora.</div>
</div>
`;
openPanel('Forenzika · '+c.naslov, html);
}
function renderAlertPanel(a){
const sevColor = a.razina==='CRITICAL'?'rd':(a.razina==='HIGH'?'am':'b');
const html = `
<div class="card-h" style="border:0;padding:0;margin-bottom:14px">
<div>
<div style="font-size:18px;font-weight:800;color:var(--t0)">[${esc(a.tip)}]</div>
<div style="font-size:12px;color:var(--t2);margin-top:4px">
<span class="tag ${sevColor}">${esc(a.razina)}</span>
${a.datum?'<span class="tag">📅 '+fmtDate(a.datum)+'</span>':''}
</div>
</div>
</div>
<div class="card">
<div class="card-h"><div class="card-t">📖 Opis alarma</div></div>
<div style="font-size:13px;line-height:1.6;color:var(--t1)">${esc(a.poruka||'')}</div>
</div>
<div class="card">
<div class="card-h"><div class="card-t">🔗 Povezani entiteti</div></div>
<table>
<tbody>
${a.klub_id ? '<tr onclick="closePanel();setTimeout(()=>openKlub('+a.klub_id+'),250)"><td><span class="tag b">Klub</span></td><td><b>Klub #'+a.klub_id+'</b></td><td>Klikni za detalje →</td></tr>' : ''}
${a.clan_id ? '<tr onclick="closePanel();setTimeout(()=>openSportas('+a.clan_id+'),250)"><td><span class="tag b">Sportaš</span></td><td><b>Sportaš #'+a.clan_id+'</b></td><td>Klikni za profil →</td></tr>' : ''}
</tbody>
</table>
${(!a.klub_id && !a.clan_id) ? '<div class="empty" style="padding:14px">Nema povezanih entiteta u alarmu</div>' : ''}
</div>
<div class="card">
<div class="card-h"><div class="card-t">📅 Metapodaci</div></div>
<div class="kv">
<div class="k">ID</div><div class="v">${esc(a.id)}</div>
<div class="k">Tip</div><div class="v">${esc(a.tip)}</div>
<div class="k">Razina</div><div class="v">${esc(a.razina)}</div>
<div class="k">Datum događaja</div><div class="v">${a.datum?fmtDate(a.datum):'—'}</div>
<div class="k">Iznos</div><div class="v">${a.iznos!=null?fmtEurFull(a.iznos):'—'}</div>
<div class="k">Riješeno</div><div class="v">${a.rijeseno?'<span class="tag gr">DA</span>':'<span class="tag rd">NE</span>'}</div>
<div class="k">Kreirano</div><div class="v">${a.created_at?fmtDate(a.created_at):'—'}</div>
</div>
</div>
`;
openPanel('Alarm #'+a.id, html);
}
async function runForensicScan(){
const inputEl = document.getElementById('fz-scan-name');
const outEl = document.getElementById('fz-scan-out');
if(!inputEl || !outEl) return;
const name = (inputEl.value||'').trim();
if(name.length < 3){ outEl.innerHTML = '<div class="empty">Unesi barem 3 znaka</div>'; return; }
outEl.innerHTML = '<div class="loading">Skeniram civic.persons… tražim povezane entitete… provjeravam forensic_findings…</div>';
const r = await apiPost('/v2/forensic/scan', {name: name});
if(!r){ outEl.innerHTML = '<div class="empty" style="color:var(--red)">Greška pri pokretanju analize</div>'; return; }
const ovr = r.overall_risk_score || 0;
const ovrCls = ovr>=70?'r':(ovr>=40?'':'g');
outEl.innerHTML = `
<div class="kpi-grid" style="grid-template-columns:repeat(4,1fr);margin-bottom:14px">
<div class="kpi ${ovrCls}"><div class="kpi-l">Overall risk</div><div class="kpi-v">${ovr}<span style="font-size:13px;color:var(--t2)">/100</span></div></div>
<div class="kpi b"><div class="kpi-l">Pronađeno osoba</div><div class="kpi-v">${r.matched_persons}</div></div>
<div class="kpi"><div class="kpi-l">Veza</div><div class="kpi-v">${r.total_links}</div></div>
<div class="kpi r"><div class="kpi-l">Findings</div><div class="kpi-v">${r.total_findings}<span style="font-size:13px;color:var(--t2)"> (${r.critical_findings} crit)</span></div></div>
</div>
${(r.persons||[]).length ? `
<div style="display:flex;flex-direction:column;gap:10px">
${r.persons.map(p => {
const cls = p.risk_score>=70?'crit':(p.risk_score>=40?'crit':'');
return `
<div class="alert-card ${cls}">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:12px">
<div style="flex:1;min-width:0">
<div class="at">${esc(p.name)} <span class="tag b">id ${p.id}</span> ${p.oib?'<a class="tag" onclick="openOIB(&quot;'+esc(p.oib)+'&quot;)" style="cursor:pointer">OIB '+esc(p.oib)+'</a>':''}</div>
<div class="ad">${p.function?esc(p.function):''}${p.party?' · '+esc(p.party):''}${p.county?' · '+esc(p.county):''}</div>
<div style="margin-top:6px;font-size:11px;color:var(--t2)">
🔗 ${(p.links||[]).length} povezanih entiteta
· ⚠ ${(p.findings||[]).length} forenzičkih nalaza
${p.trust_tier!=null?' · trust tier '+p.trust_tier:''}
</div>
${(p.links||[]).length ? '<div style="margin-top:8px;font-size:11px"><b style="color:var(--t2)">Veze:</b> '+
p.links.slice(0,8).map(l => '<span class="tag b" style="margin-right:3px">'+esc(l.entity_name||'#'+l.entity_id)+(l.roles?' · '+esc(l.roles):'')+'</span>').join('')+
((p.links||[]).length>8?' <span class="tag">+'+((p.links||[]).length-8)+' više</span>':'')+
'</div>' : ''}
${(p.findings||[]).length ? '<div style="margin-top:8px;font-size:11px"><b style="color:var(--t2)">Nalazi:</b><br>'+
p.findings.slice(0,5).map(f => '<div style="margin-top:3px"><span class="tag '+(f.severity==='CRITICAL'?'rd':f.severity==='HIGH'?'am':'b')+'">'+esc(f.severity)+'</span> '+esc(f.title||f.finding_type)+'</div>').join('')+
'</div>' : ''}
</div>
<div style="text-align:center;flex-shrink:0">
<div style="font-size:24px;font-weight:800;color:${p.risk_score>=70?'var(--red)':p.risk_score>=40?'var(--amber)':'var(--green)'};font-family:var(--mono)">${p.risk_score}</div>
<div style="font-size:10px;color:var(--t4);text-transform:uppercase">RISK</div>
</div>
</div>
</div>`;
}).join('')}
</div>
` : '<div class="empty">Nema pronađenih osoba s tim imenom</div>'}
`;
}
//=========== INIT ===========
function init(){
restoreSidebar();
buildNav();
navTo('dashboard');
}
window.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>