Files
pgz-sport/static/sport2.html.s4.new

4440 lines
235 KiB
Plaintext
Raw Permalink 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>
<!-- 3d-force-graph bundles Three.js internally; standalone removed to avoid duplicate import -->
<script src="https://unpkg.com/3d-force-graph@1.73.4/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}
/* Native sidebar replaced by shared /static/shared/sidebar.* — kept hidden but DOM intact for legacy buildNav() calls */
.sb{display:none}
.sb-old{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)}
.sb.collapsed .nav-sep{font-size:0;padding:6px 0;text-align:center;border-top:1px dashed var(--rim);margin:6px 8px 4px}
.sb.collapsed .nav-ext span:last-child{display:none}
.main{margin-left:0;flex:1;min-width:0;transition:margin-left .22s ease}
/* body.pgz-has-sb (from shared/sidebar.css) provides the left padding */
.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{width:100%;aspect-ratio:4/5;overflow:hidden;background:var(--bg3);position:relative;display:flex;align-items:center;justify-content:center}
.player-card .ph img{width:100%;height:100%;object-fit:cover;object-position:center 25%}
.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}
/* RUSH-2 2026-05-05: small inline avatar (left of name) */
.player-card .pn-row{display:flex;align-items:center;gap:8px}
.player-card .pn-row .pn{flex:1;min-width:0}
.rush2-avatar{display:inline-flex;align-items:center;justify-content:center;border-radius:50%;overflow:hidden;background:var(--bg3);border:1px solid var(--rim);flex-shrink:0;color:var(--pgz-gold);font-weight:800;letter-spacing:.5px}
.rush2-avatar img{width:100%;height:100%;object-fit:cover;display:block}
.rush2-avatar.r2a-fb{background:linear-gradient(135deg,#1a1f2e,#2a3046);color:var(--pgz-gold)}
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)}
/* BUG-E (2026-05-05) — filter-bar above section toolbar */
.bugE-bar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin:0 0 10px 0;padding:10px 12px;background:linear-gradient(180deg,rgba(20,30,48,.6),rgba(15,22,36,.5));border:1px solid var(--rim);border-left:3px solid var(--pgz-gold);border-radius:6px}
.bugE-bar .bugE-lbl{font-size:10px;color:var(--pgz-gold);font-weight:800;letter-spacing:1.4px}
.bugE-bar label{font-size:11px;color:var(--t1);display:flex;align-items:center;gap:6px;cursor:pointer;user-select:none}
.bugE-bar label small{color:var(--t2);font-weight:400}
.bugE-bar input[type=checkbox]{accent-color:var(--pgz-gold)}
.bugE-bar input[type=number]{background:var(--bg2);border:1px solid var(--rim);border-radius:4px;padding:5px 8px;color:var(--t1);font-size:12px}
.bugE-bar .btn{padding:6px 12px;font-size:11px}
.bugE-bar .bugE-cnt{margin-left:auto;font-size:11px;color:var(--t2);font-weight:600;letter-spacing:.5px}
.bugE-bar .bugE-cnt strong{color:var(--pgz-gold)}
.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:0;width:600px;max-width:96vw;height:100vh;background:var(--bg1);border-left:1px solid var(--rim);z-index:200;transform:translateX(100%);visibility:hidden;transition:transform .25s ease,visibility 0s linear .25s;display:flex;flex-direction:column;box-shadow:-8px 0 30px rgba(0,0,0,.5)}
#panel.open{transform:translateX(0);visibility:visible;transition:transform .25s ease,visibility 0s linear 0s}
#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}
.pp-bio-row{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px;font-size:11px;color:var(--t2)}
.pp-bio-chip{padding:3px 8px;background:var(--bg3);border:1px solid var(--rim);border-radius:4px;color:var(--t1);font-weight:500}
.pp-bio-chip b{color:var(--pgz-gold);font-family:var(--mono);font-weight:700;margin-right:4px}
.pp-links{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px}
.pp-link{display:inline-flex;align-items:center;gap:6px;padding:6px 11px;background:var(--bg3);border:1px solid var(--rim2);border-radius:5px;color:var(--t1);font-size:11.5px;font-weight:600;text-decoration:none;cursor:pointer;transition:all .15s}
.pp-link:hover{background:var(--bg4);border-color:var(--pgz-gold);color:var(--pgz-gold);transform:translateY(-1px)}
.pp-link.hns{border-color:#0066cc;color:#3aa8ff}
.pp-link.hns:hover{background:rgba(0,102,204,.15);color:#5cc8ff;border-color:#3aa8ff}
.pp-link.gg{border-color:#9b8aff;color:#b9aeff}
.pp-link.gg:hover{background:rgba(155,138,255,.15);color:#cfc6ff}
.pp-link.wiki{border-color:#aaa;color:#ddd}
.pp-link.wiki:hover{background:rgba(255,255,255,.06);color:#fff}
.pp-section{margin-top:18px}
.pp-section-h{display:flex;align-items:center;gap:8px;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid var(--rim);font-size:13px;font-weight:700;color:var(--t0);letter-spacing:.3px}
.pp-section-h .cnt{font-size:10.5px;color:var(--t4);font-weight:600;font-family:var(--mono)}
.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}
/* HNS-3 (2026-05-05) — Profil tab styles (Palantir Gotham aesthetic) */
.prof-top{display:flex;align-items:flex-start;gap:18px;padding:14px;background:var(--bg2);border:1px solid var(--rim);border-radius:6px;margin-bottom:14px}
.prof-name-block{flex:1;min-width:0}
.prof-name{font-size:20px;font-weight:800;color:var(--t0);letter-spacing:.3px;margin-bottom:4px}
.prof-sub{color:var(--t2);font-size:12px;margin-bottom:6px}
.prof-club{font-size:12.5px;color:var(--t1);display:flex;align-items:center;flex-wrap:wrap;gap:6px}
.prof-dres{flex-shrink:0;width:96px;height:96px;border:2px solid var(--pgz-gold);border-radius:8px;display:flex;flex-direction:column;align-items:center;justify-content:center;background:linear-gradient(135deg,var(--bg3),var(--bg2))}
.prof-dres-num{font-size:38px;font-weight:900;color:var(--pgz-gold);line-height:1;font-family:var(--mono)}
.prof-dres-l{font-size:9.5px;color:var(--t3);letter-spacing:1.5px;font-weight:700;margin-top:4px}
.prof-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:6px}
.prof-cell{padding:9px 12px;background:var(--bg2);border:1px solid var(--rim);border-radius:5px}
.prof-cell .l{font-size:10px;color:var(--t3);text-transform:uppercase;letter-spacing:.5px;font-weight:700;margin-bottom:3px}
.prof-cell .v{font-size:13px;color:var(--t0);font-weight:600;word-break:break-word}
.prof-cell .v b{color:var(--pgz-gold);font-family:var(--mono)}
@media (max-width:760px){
.prof-grid{grid-template-columns:repeat(2,1fr)}
.prof-top{flex-direction:column-reverse;align-items:stretch}
.prof-dres{width:100%;height:78px;flex-direction:row;gap:14px}
.prof-dres-num{font-size:32px}
}
@media (max-width:420px){
.prof-grid{grid-template-columns:1fr}
}
.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)}
.ac-wrap{position:relative}
.ac-drop{display:none;position:absolute;top:100%;left:0;right:0;min-width:240px;background:var(--bg2);border:1px solid var(--rim2);border-radius:5px;box-shadow:0 6px 20px rgba(0,0,0,.5);z-index:100;max-height:300px;overflow-y:auto;margin-top:4px}
.ac-item{padding:8px 12px;cursor:pointer;border-bottom:1px solid var(--rim);transition:background .12s}
.ac-item:last-child{border-bottom:0}
.ac-item:hover{background:var(--bg3)}
.ac-item .ac-l{font-weight:600;color:var(--t0);font-size:12.5px}
.ac-item .ac-s{font-size:10.5px;color:var(--t2);margin-top:2px}
.ac-empty{padding:12px;text-align:center;color:var(--t4);font-size:11px;font-style:italic}
@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)}
}
/* === MOBILE RESPONSIVE (CRISIS FIX) === */
@media (max-width: 768px) {
body { font-size: 13px; }
.header { flex-direction: column !important; gap: 8px !important; padding: 10px !important; }
.header h1 { font-size: 16px !important; }
.nav-tabs { overflow-x: auto; white-space: nowrap; -webkit-overflow-scrolling: touch; }
.nav-tabs .tab { display: inline-block !important; }
.card { padding: 10px !important; }
.kpi-grid { grid-template-columns: 1fr 1fr !important; gap: 6px !important; }
.kpi-v { font-size: 18px !important; }
.klubovi-grid, .grid-2, .grid-3, .grid-4 {
grid-template-columns: 1fr !important;
}
/* Tables → horizontal scroll */
table { font-size: 11px !important; min-width: 480px; }
.table-container, .card { overflow-x: auto; }
/* Drill-down panel full-width */
#panel { width: 100vw !important; max-width: 100vw !important; right: 0 !important; }
#panel.open { right: 0 !important; }
/* Buttons */
.btn { padding: 8px 12px !important; font-size: 13px !important; }
/* Center mobile content */
.container, main { padding: 8px !important; max-width: 100% !important; margin: 0 !important; }
}
/* PANEL EXPAND (CRISIS V6) — drill-down panel BIGGER za HNS karijera */
#panel, #dpanel {
width: 70vw !important;
max-width: 1100px !important;
min-width: 600px !important;
}
@media (min-width: 1400px){
#panel, #dpanel { width: 60vw !important; }
}
@media (max-width: 768px){
#panel, #dpanel {
width: 100vw !important;
max-width: 100vw !important;
min-width: 0 !important;
}
}
/* HNS karijera tabela full-width */
#panel table, #dpanel table { width: 100%; font-size: 12px; }
#panel .hns-stats td { padding: 4px 6px; }
/* PANEL FORCE HIDE (CRISIS V7) — uvijek skriven dok nije .open */
#panel:not(.open) { right: -100vw !important; transform: translateX(0) !important; }
#panel.open { right: 0 !important; }
#panel-overlay:not(.open) { display: none !important; }
#panel-overlay.open { display: block !important; }
</style>
<link rel="stylesheet" href="/static/shared/sidebar.css">
<script src="/static/shared/sidebar.js" defer data-active="dashboard"></script>
<script src="/static/oib_format.js" defer></script>
</head>
<body>
<div class="app">
<aside class="sb" id="sb">
<div class="sb-h">
<a href="/" class="logo" style="text-decoration:none;color:inherit;cursor:pointer" title="Početna"><span style="font-weight:800;letter-spacing:.5px">PGŽ</span> <span class="g">SPORT</span></a>
<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-igraci-kat" class="section"></section>
<section id="pg-financije" class="section"></section>
<section id="pg-objekti" class="section"></section>
<section id="pg-manifestacije" class="section"></section>
<section id="pg-mreza" class="section"></section>
<section id="pg-forenzika" class="section"></section>
<section id="pg-audit" class="section"></section>
</div>
</main>
</div>
<div id="panel-overlay" onclick="closePanel()"></div>
<div id="panel">
<div id="panel-hdr">
<div style="display:flex;align-items:center;gap:8px">
<button id="panel-back" onclick="panelBack()" style="background:var(--bg3);border:1px solid var(--rim);color:var(--t1);padding:5px 10px;border-radius:4px;cursor:pointer;display:none" title="Nazad">← Natrag</button>
<div id="panel-hdr-t">Detalji</div>
</div>
<div id="panel-x" onclick="closePanel()" title="Zatvori">×</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:'igraci-kat', ic:'\u{1F3F7}', label:'Po kategoriji'},
{id:'financije', ic:'€', label:'Financije'},
{id:'objekti', ic:'\u{1F4CD}', label:'Objekti'},
{id:'manifestacije', ic:'\u{1F4C5}', label:'Manifestacije'},
{id:'mreza', ic:'\u{1F578}', label:'Mreža'},
{id:'forenzika', ic:'⚠', label:'Forenzika'},
{id:'audit', ic:'\u{1F512}', label:'Audit log'}
];
const SECTION_TITLES = {
dashboard: ['Dashboard', 'Pregled stanja PGŽ Sporta'],
savezi: ['Savezi', '246 sportskih saveza'],
klubovi: ['Klubovi', 'Sportski klubovi PGŽ'],
sportasi: ['Sportaši', 'Registrirani članovi'],
'igraci-kat': ['Igrači po kategoriji', 'Grupirani po dobnoj/natjecateljskoj kategoriji'],
financije: ['Financije', 'Sufinanciranje sporta'],
objekti: ['Sportski objekti', 'Geocodirana infrastruktura'],
manifestacije: ['Manifestacije', 'Sportski događaji'],
mreza: ['Mreža', 'Force-directed graf entiteta i veza'],
forenzika: ['Forenzika', 'Kritični nalazi i alarmi'],
audit: ['Audit log', 'Polygon PoS pečaćenje ključnih akcija']
};
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;
// ════════════════════════════════════════════════════════════════════
// BUG-E (2026-05-05) — explicit filter-bar state per section
// Author: Damir Radulić (dradulic@outlook.com / damir@rinet.one)
// Defaults match constitution: financirani=true + u-godišnjaku=true.
// User can uncheck either checkbox to broaden the result set.
// ════════════════════════════════════════════════════════════════════
const _filters = {
klubovi: { financirani: true, godisnjak: true, hns_roster: false, total: 0 },
sportasi: { priority: true, hns_profil: false, godina_od: null, godina_do: null, total: 0 },
savezi: { financirani: true, total: 0 }
};
function _filtersDefaults(sec){
if(sec==='klubovi') return { financirani:true, godisnjak:true, hns_roster:false };
if(sec==='sportasi') return { priority:true, hns_profil:false, godina_od:null, godina_do:null };
if(sec==='savezi') return { financirani:true };
return {};
}
function _filtersReset(sec){
Object.assign(_filters[sec], _filtersDefaults(sec));
_filtersApply(sec);
}
function _filtersApply(sec){
if(sec==='klubovi') { _cache.klubovi = null; loadKlubovi(); }
if(sec==='sportasi') { _cache.clanovi = null; loadSportasi(); }
if(sec==='savezi') { _cache.savezi = null; loadSavezi(); }
}
function _filtersBar(sec){
// Returns HTML for the BUG-E filter-bar above the existing toolbar.
const f = _filters[sec] || {};
const cnt = '<span class="bugE-cnt" id="bugE-cnt-'+sec+'">Prikazano: '
+ (f.shown||0) + ' od ' + (f.total||0) + '</span>';
if(sec==='klubovi'){
return `
<div class="bugE-bar">
<span class="bugE-lbl">FILTER:</span>
<label><input type="checkbox" ${f.financirani?'checked':''} onchange="_filters.klubovi.financirani=this.checked"> Samo financirani <small>(PGŽ + RSS + Grad Rijeka)</small></label>
<label><input type="checkbox" ${f.godisnjak?'checked':''} onchange="_filters.klubovi.godisnjak=this.checked"> U godišnjaku</label>
<label><input type="checkbox" ${f.hns_roster?'checked':''} onchange="_filters.klubovi.hns_roster=this.checked"> Ima HNS roster</label>
<button class="btn primary" onclick="_filtersApply('klubovi')">Primijeni</button>
<button class="btn" onclick="_filtersReset('klubovi')">Reset</button>
${cnt}
</div>`;
}
if(sec==='sportasi'){
return `
<div class="bugE-bar">
<span class="bugE-lbl">FILTER:</span>
<label><input type="checkbox" ${f.priority?'checked':''} onchange="_filters.sportasi.priority=this.checked"> Samo iz priority kluba</label>
<label><input type="checkbox" ${f.hns_profil?'checked':''} onchange="_filters.sportasi.hns_profil=this.checked"> Ima HNS profil</label>
<label class="bugE-range">Godina rođ. od: <input type="number" min="1900" max="2030" value="${f.godina_od||''}" placeholder="—" onchange="_filters.sportasi.godina_od=this.value?parseInt(this.value,10):null" style="width:90px"></label>
<label class="bugE-range">do: <input type="number" min="1900" max="2030" value="${f.godina_do||''}" placeholder="—" onchange="_filters.sportasi.godina_do=this.value?parseInt(this.value,10):null" style="width:90px"></label>
<button class="btn primary" onclick="_filtersApply('sportasi')">Primijeni</button>
<button class="btn" onclick="_filtersReset('sportasi')">Reset</button>
${cnt}
</div>`;
}
if(sec==='savezi'){
return `
<div class="bugE-bar">
<span class="bugE-lbl">FILTER:</span>
<label><input type="checkbox" ${f.financirani?'checked':''} onchange="_filters.savezi.financirani=this.checked"> Samo financirani</label>
<button class="btn primary" onclick="_filtersApply('savezi')">Primijeni</button>
<button class="btn" onclick="_filtersReset('savezi')">Reset</button>
${cnt}
</div>`;
}
return '';
}
function _filtersUpdateCount(sec, shown){
_filters[sec].shown = shown;
const el = document.getElementById('bugE-cnt-'+sec);
if(el) el.textContent = 'Prikazano: '+shown+' od '+(_filters[sec].total||shown);
}
window._filters = _filters;
window._filtersReset = _filtersReset;
window._filtersApply = _filtersApply;
// === PGŽ priority filter (SUB6) — global helper, works across Klubovi/Savezi/Sportaši ===
window._pgz_filter_priority = window._pgz_filter_priority || false;
window.togglePGZFilter = function(section){
window._pgz_filter_priority = !window._pgz_filter_priority;
// Drop caches so the next load fetches priority-only or full set.
if(!section || section==='klubovi') _cache.klubovi = null;
if(!section || section==='savezi') _cache.savezi = null;
if(!section || section==='sportasi') _cache.clanovi = null;
// Reload whichever section is active.
const cur = (section || _state.section);
if(cur==='klubovi') loadKlubovi();
else if(cur==='savezi') loadSavezi();
else if(cur==='sportasi') loadSportasi();
else loadSection(_state.section);
};
window.pgzBadgePrefix = function(it, kind){
// returns ⭐💰📖 prefix tailored to which markers apply
const fin = !!(it && (it.financiran || it.klub_financiran || it.pgz_sufinanciran));
const god = !!(it && (it.godisnjak || it.klub_godisnjak || (it.godisnjak_godine && (it.godisnjak_godine.length||0)>0)));
const pgzs = !!(it && it.pgz_relevant);
const pri = !!(it && it.priority) || fin || god || pgzs;
if(!pri) return '';
let s = '⭐';
if(fin || pgzs) s += '💰';
if(god) s += '📖';
return s + ' ';
};
window.renderPGZToggleBtn = function(section){
const on = !!window._pgz_filter_priority;
return '<button class="btn '+(on?'primary':'')+'" '
+ 'title="Prikaži samo PGŽ-financirane / u godišnjaku" '
+ 'onclick="togglePGZFilter(\''+section+'\')">'
+ (on ? '⭐ PGŽ filter ON' : '☆ PGŽ filter OFF') + '</button>';
};
//=========== 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 tok = localStorage.getItem('pgz_access') || sessionStorage.getItem('pgz_access') || localStorage.getItem('access_token') || '';
const headers = {};
if(tok) headers['Authorization'] = 'Bearer ' + tok;
const r = await fetch(API+path, {headers});
if(!r.ok) throw new Error('HTTP '+r.status);
return await r.json();
}catch(e){
console.error('API GET error', path, e);
return null;
}
}
async function apiPost(path, body, opts){
// apiPost: 30s timeout (added 2026-05-10) — protects against slow enrich endpoints.
// Pass {timeoutMs: N} to override.
const timeoutMs = (opts && opts.timeoutMs) || 30000;
const ctrl = new AbortController();
const tid = setTimeout(() => ctrl.abort(), timeoutMs);
try{
const tok = localStorage.getItem('pgz_access') || sessionStorage.getItem('pgz_access') || localStorage.getItem('access_token') || '';
const headers = {'Content-Type':'application/json'};
if(tok) headers['Authorization'] = 'Bearer ' + tok;
const r = await fetch(API+path, {method:'POST', headers, signal: ctrl.signal, body: body?JSON.stringify(body):'{}'});
clearTimeout(tid);
if(!r.ok){
const errText = await r.text().catch(()=>(''));
throw new Error('HTTP '+r.status+(errText? ': '+errText.slice(0,150):''));
}
return await r.json();
}catch(e){
clearTimeout(tid);
if(e.name === 'AbortError'){
const msg = `Timeout (${(timeoutMs/1000)|0}s) — server presporo odgovara`;
console.error('API POST timeout', path);
if(typeof showToast === 'function') showToast(msg, 'err');
return null;
}
console.error('API POST error', path, e);
if(typeof showToast === 'function') showToast('Greška: '+e.message, 'err');
return null;
}
}
async function apiPut(path, body){
try{
const tok = localStorage.getItem('pgz_access') || sessionStorage.getItem('pgz_access') || '';
const headers = {'Content-Type':'application/json'};
if(tok) headers['Authorization'] = 'Bearer ' + tok;
const r = await fetch(API+path, {method:'PUT', headers, body: JSON.stringify(body||{})});
if(!r.ok) throw new Error('HTTP '+r.status);
return await r.json();
}catch(e){ console.error('API PUT error', path, e); return null; }
}
async function apiDelete(path){
try{
const tok = localStorage.getItem('pgz_access') || sessionStorage.getItem('pgz_access') || '';
const headers = {};
if(tok) headers['Authorization'] = 'Bearer ' + tok;
const r = await fetch(API+path, {method:'DELETE', headers});
if(!r.ok) throw new Error('HTTP '+r.status);
return await r.json();
}catch(e){ console.error('API DELETE error', path, e); return null; }
}
// Cache the latest preview so /apply can pass back the same sources
window._enrichPreviews = window._enrichPreviews || {};
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; }
window._enrichPreviews[kind+':'+id] = r;
const cov = r.coverage||0;
const covCls = cov>=70?'high':(cov>=40?'mid':'low');
const proposed = r.proposed || {};
const current = r.current || {};
const propKeys = Object.keys(proposed);
const lastEnr = r.last_enriched_at
? `<span class="tb-s" title="${esc(r.last_enriched_at)}">✓ Obogaćeno ${esc(String(r.last_enriched_at).slice(0,10))}</span>`
: '';
let diffHtml = '';
if(propKeys.length){
const rows = propKeys.map(k => {
const cv = current[k]; const pv = proposed[k];
const cvHtml = cv
? '<span style="color:var(--t1)">'+esc(String(cv).slice(0,200))+'</span>'
: '<span class="tag rd">prazno</span>';
const pvHtml = '<span style="color:var(--ok)">'+esc(String(pv).slice(0,400))+'</span>';
return `<tr>
<td style="vertical-align:top;padding:6px 8px"><label style="display:flex;gap:6px;align-items:center;cursor:pointer">
<input type="checkbox" data-field="${esc(k)}" checked style="width:16px;height:16px"> <b style="font-family:monospace;font-size:12px">${esc(k)}</b>
</label></td>
<td style="vertical-align:top;padding:6px 8px;font-size:12px;max-width:240px">${cvHtml}</td>
<td style="vertical-align:top;padding:6px 8px;font-size:12px">${pvHtml}</td>
</tr>`;
}).join('');
diffHtml = `
<div style="margin:10px 0;border:1px solid var(--ln);border-radius:6px;overflow:hidden">
<div style="padding:8px 10px;background:var(--bg3);font-size:11px;color:var(--t4);text-transform:uppercase;letter-spacing:.5px">📋 Predložene izmjene (uncheck za preskočiti)</div>
<table style="width:100%;border-collapse:collapse;font-size:12px">
<thead><tr style="background:var(--bg2)"><th style="text-align:left;padding:6px 8px;width:160px">Polje</th><th style="text-align:left;padding:6px 8px;width:240px">Trenutno</th><th style="text-align:left;padding:6px 8px">Predloženo</th></tr></thead>
<tbody id="enrich-diff-${kind}-${id}">${rows}</tbody>
</table>
<div style="padding:8px 10px;background:var(--bg2);display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap">
<button class="btn" onclick="enrichSelectAll('${kind}',${id},true)">Označi sve</button>
<button class="btn" onclick="enrichSelectAll('${kind}',${id},false)">Poništi sve</button>
<button class="btn" onclick="document.getElementById('enrich-out-${kind}-${id}').innerHTML=''">❌ Odustani</button>
<button class="btn primary" onclick="enrichApply('${kind}',${id})">💾 SPREMI IZMJENE</button>
</div>
</div>`;
} else {
diffHtml = '<div class="empty" style="padding:14px">Nema novih predloženih dopuna iz vanjskih izvora.</div>';
}
const sourcesHtml = (r.sources||[]).map(s => `
<div style="padding:8px 10px;background:var(--bg3);border-left:3px solid var(--pgz-gold);border-radius:4px;margin-bottom:6px">
<div style="font-size:11px;color:var(--t4);text-transform:uppercase;letter-spacing:.5px">${esc(s.source||'')}</div>
${s.title ? '<div style="font-weight:700;color:var(--t0);font-size:13px">'+esc(s.title)+'</div>' : ''}
${s.extract ? '<div style="font-size:11.5px;color:var(--t1);line-height:1.5">'+esc(String(s.extract).slice(0,300))+'…</div>' : ''}
${s.url ? '<div style="margin-top:4px"><a href="'+esc(s.url)+'" target="_blank" style="font-size:11px">↗ '+esc(String(s.url).slice(0,90))+'</a></div>' : ''}
</div>`).join('');
const html = `
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;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>
${lastEnr}
</div>
${diffHtml}
${sourcesHtml ? `<div style="margin:10px 0">
<div style="font-size:11px;color:var(--t4);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px">🔗 Izvori</div>
${sourcesHtml}
</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 enrichSelectAll(kind, id, on){
const tbody = document.getElementById('enrich-diff-'+kind+'-'+id);
if(!tbody) return;
tbody.querySelectorAll('input[type=checkbox]').forEach(cb => { cb.checked = !!on; });
}
// Reusable toast component (success / error / info / warn).
window.toast = function(msg, type, duration){
type = type || 'success';
duration = duration || 3000;
const palette = {
success: ['#1ec773', '#0b1a16'],
error: ['#ff6b6b', '#1a0b0b'],
info: ['#4a9eff', '#04132b'],
warn: ['#ffb84a', '#1a1004'],
}[type] || ['#4a9eff', '#04132b'];
const t = document.createElement('div');
t.className = 'pgz-toast pgz-toast-' + type;
t.style.cssText = 'position:fixed;right:20px;bottom:20px;'+
'background:'+palette[0]+';color:'+palette[1]+';'+
'padding:12px 18px;border-radius:8px;font-weight:700;font-size:14px;'+
'z-index:99999;box-shadow:0 6px 22px rgba(0,0,0,.45);'+
'transform:translateY(40px);opacity:0;transition:all .25s ease-out;'+
'max-width:380px;line-height:1.45;';
t.innerHTML = msg;
document.body.appendChild(t);
requestAnimationFrame(()=>{ t.style.transform='translateY(0)'; t.style.opacity='1'; });
setTimeout(()=>{
t.style.transform='translateY(40px)'; t.style.opacity='0';
setTimeout(()=>t.remove(), 280);
}, duration);
return t;
};
async function enrichApply(kind, id){
const target = document.getElementById('enrich-out-'+kind+'-'+id);
const tbody = document.getElementById('enrich-diff-'+kind+'-'+id);
const preview = (window._enrichPreviews||{})[kind+':'+id];
if(!preview){ toast('⚠ Prvo pokreni "▶ Pokreni"', 'warn'); return; }
const proposed = preview.proposed || {};
const fields = {};
if(tbody){
tbody.querySelectorAll('input[type=checkbox]:checked').forEach(cb => {
const f = cb.getAttribute('data-field');
if(f && proposed[f] !== undefined) fields[f] = proposed[f];
});
} else {
Object.assign(fields, proposed);
}
if(!Object.keys(fields).length){ toast('Označi barem jedno polje za primjenu.', 'warn'); return; }
if(target) target.innerHTML = '<div class="loading">⏳ Spremam u bazu…</div>';
try{
const r = await fetch(API+'/v2/enrich/'+kind+'/'+id+'/apply', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({fields, sources: preview.sources || []}),
});
const data = await r.json();
if(!r.ok){ throw new Error(data.detail || ('HTTP '+r.status)); }
// Refresh the entire detail panel so the new values render
if(kind === 'klub' && typeof openKlub === 'function') await openKlub(id);
else if(kind === 'savez' && typeof openSavez === 'function') await openSavez(id);
else if(kind === 'sportas' && typeof openSportas === 'function') await openSportas(id);
setTimeout(() => enrichEntity(kind, id), 350);
const cnt = data.applied_count != null ? data.applied_count : Object.keys(data.applied||{}).length;
const fieldsList = (data.applied_fields || Object.keys(data.applied||{})).join(', ');
if(cnt){
toast('✅ Spremljeno <b>'+cnt+'</b> polja u bazu'
+ (fieldsList ? '<br><span style="opacity:.85;font-weight:500;font-size:12px">'+esc(fieldsList)+'</span>' : ''),
'success', 3500);
} else {
toast('Nema novih izmjena za spremiti.', 'info', 2500);
}
}catch(e){
console.error(e);
toast('❌ Greška pri spremanju: '+esc(e.message||String(e)), 'error', 4500);
if(target) target.innerHTML = '<div class="empty" style="color:var(--bad,#ff6b6b)">Greška pri spremanju: '+esc(e.message||String(e))+'</div>';
}
}
// Bulk enrichment — used by "Obogati sve" buttons in list views
async function enrichBulk(kind, limit, coverage_max){
limit = limit || 50; coverage_max = coverage_max || 70;
if(!confirm('Pokreni obogaćivanje za '+limit+' nasumično odabranih ('+kind+', coverage<'+coverage_max+'%)?')) return;
toast('⏳ Pokrećem bulk obogaćivanje za '+limit+' '+kind+'…', 'info', 2500);
try{
const r = await fetch(API+'/v2/enrich/bulk', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({kind, limit, coverage_max}),
});
const data = await r.json();
if(!r.ok) throw new Error(data.detail || ('HTTP '+r.status));
toast('✅ Bulk gotov: <b>'+data.processed+'</b>/'+data.requested+' obrađeno, '+
'dodano <b>'+data.fields_total+'</b> polja u DB ('+data.elapsed_s+'s)',
'success', 5000);
// Reload the section so new values appear
if(typeof loadSection === 'function' && _state && _state.section) loadSection(_state.section);
}catch(e){
console.error(e);
toast('❌ Bulk greška: '+esc(e.message||String(e)), 'error', 5000);
}
}
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 'igraci-kat': return applyIgraciKatFilter && applyIgraciKatFilter();
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('');
}
// Hashchange handler — accept routing from unified shared sidebar
window.addEventListener('hashchange', () => {
const h = (location.hash||'').replace(/^#/,'');
if(h && NAV_ITEMS.some(n => n.id===h)) navTo(h);
});
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 'igraci-kat': return loadIgraciKat();
case 'financije': return loadFinancije();
case 'objekti': return loadObjekti();
case 'manifestacije': return loadManifestacije();
case 'mreza': return loadMreza();
case 'forenzika': return loadForenzika();
case 'audit': return loadAudit();
}
}
//=========== AUDIT LOG (Polygon PoS) ===========
async function loadAudit(){
const root = $('#pg-audit');
root.innerHTML = '<div class="loading">Učitavanje audit zapisa…</div>';
let r;
try{
const resp = await fetch(API+'/audit/seal/list?limit=100');
if(!resp.ok) throw new Error('HTTP '+resp.status);
r = await resp.json();
}catch(e){
root.innerHTML = '<div class="empty">Greška: '+esc(e.message||String(e))+'</div>';
return;
}
const wallet = r.wallet || '';
const live = r.live ? '<span class="tag gr">🟢 LIVE Polygon</span>' : '<span class="tag gd">⏳ Pending mode</span>';
const rows = (r.rows||[]).map(s => {
const tx = s.tx_hash || '';
const isPending = tx.startsWith('pending:');
const txCell = isPending
? `<span class="tag gd" title="${esc(tx)}">PENDING</span><span class="tb-s" style="margin-left:6px;font-family:monospace">${esc(tx.slice(0,32))}…</span>`
: `<a href="${esc(s.polygonscan_url||'#')}" target="_blank" style="font-family:monospace;font-size:11px">${esc(tx.slice(0,18))}…${esc(tx.slice(-6))}</a>`;
const statusCls = s.status==='confirmed'?'gr':(s.status==='broadcast'?'gd':(s.status==='failed'?'rd':''));
return `<tr>
<td class="num" style="font-size:11px;color:var(--t3)">${s.id}</td>
<td style="font-size:11px;color:var(--t3)">${esc((s.created_at||'').replace('T',' ').slice(0,19))}</td>
<td><b>${esc(s.action)}</b></td>
<td style="font-size:11px">${esc(s.ref_type||'')} <span style="color:var(--t3)">${esc(s.ref_id||'')}</span></td>
<td style="font-family:monospace;font-size:10.5px;color:var(--t2)" title="${esc(s.data_hash||'')}">${esc((s.data_hash||'').slice(0,12))}…</td>
<td>${txCell}</td>
<td><span class="tag ${statusCls}">${esc(s.status||'')}</span></td>
<td style="font-size:11px">${esc(s.user_email||'')}</td>
</tr>`;
}).join('');
root.innerHTML = `
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:14px">
<div class="card" style="flex:1;min-width:280px">
<div class="card-h"><div class="card-t">🔐 Polygon PoS Audit Sealing</div></div>
<div style="padding:12px;font-size:13px;color:var(--t1);line-height:1.6">
Ključne akcije (odobrenje sufinanciranja, isplata, validacija liječničkog pregleda, izmjena članstva)
pečate se 0-MATIC self-tx-om s SHA-256 hash-em payload-a u <code>data</code> polju.
Svaki zapis je nepromjenjiv i provjerljiv preko polygonscan.com.
</div>
<div style="padding:0 12px 12px;font-size:12px">
<div><b>Wallet:</b> <a href="https://polygonscan.com/address/${esc(wallet)}" target="_blank" style="font-family:monospace">${esc(wallet)}</a></div>
<div><b>Chain:</b> Polygon PoS (137)</div>
<div><b>Mode:</b> ${live}</div>
</div>
</div>
<div class="card" style="min-width:200px">
<div class="card-h"><div class="card-t">📊 Statistika</div></div>
<div style="padding:12px;font-size:14px">
<div><b>${r.count||0}</b> sealed zapisa</div>
<div style="color:var(--t3);font-size:12px;margin-top:4px">Najnoviji prikazani prvi</div>
</div>
</div>
</div>
<div id="enrich-worker-card"></div>
${rows ? `
<div class="card">
<div class="card-h"><div class="card-t">📜 Audit zapisi</div></div>
<div class="tbl-wrap"><table>
<thead><tr>
<th class="num">#</th><th>Vrijeme</th><th>Akcija</th><th>Referenca</th>
<th>SHA-256</th><th>Polygon TX</th><th>Status</th><th>Korisnik</th>
</tr></thead>
<tbody>${rows}</tbody>
</table></div>
</div>` : '<div class="empty">Nema audit zapisa.</div>'}
`;
loadEnrichWorker();
}
// ─── 24/7 enrichment worker dashboard ───────────────────────────────
let _enrichWorkerTimer = null;
async function loadEnrichWorker(){
const card = document.getElementById('enrich-worker-card');
if(!card) return;
let s;
try{
const resp = await fetch(API+'/v2/enrich/worker/status');
if(!resp.ok) throw new Error('HTTP '+resp.status);
s = await resp.json();
}catch(e){
card.innerHTML = '<div class="card"><div class="card-h"><div class="card-t">🤖 Enrichment Worker</div></div><div class="empty">Status nedostupan: '+esc(e.message||String(e))+'</div></div>';
return;
}
const conf = s.confidence_threshold != null ? s.confidence_threshold : 0.7;
const hbAge = s.heartbeat_age_s;
const hbBadge = hbAge == null
? '<span class="tag gd">⏳ Pokreće se…</span>'
: (hbAge < 120 ? '<span class="tag gr">🟢 AKTIVAN</span>' : '<span class="tag rd">🔴 ZAUSTAVLJEN</span>');
const pauseBtn = s.paused
? '<button class="btn primary" onclick="setWorkerPause(false)">▶️ Nastavi</button>'
: '<button class="btn" onclick="setWorkerPause(true)">⏸ Pauziraj</button>';
const lc = s.last_cycle || {};
const recent = (s.recent || []).slice(0, 8);
const recentRows = recent.map(r => `
<tr>
<td style="font-size:11px;color:var(--t3)">${esc((r.created_at||'').replace('T',' ').slice(0,19))}</td>
<td><b>${esc(r.kind||'')}</b> #${r.target_id}</td>
<td style="font-size:11px;color:var(--t2)">${esc((r.fields_set||[]).join(', '))}</td>
<td style="font-size:11px;color:var(--t3)">${esc(r.source||'')}</td>
</tr>`).join('');
card.innerHTML = `
<div class="card" style="margin-bottom:14px">
<div class="card-h" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<div class="card-t">🤖 Enrichment Worker</div>
${hbBadge}
${s.paused ? '<span class="tag gd">PAUZIRAN</span>' : ''}
<span class="tb-s">${hbAge != null ? 'Zadnji heartbeat: '+hbAge+'s' : ''}</span>
<span style="margin-left:auto;display:flex;gap:6px;flex-wrap:wrap">
<button class="btn primary" onclick="runWorkerNow()">▶ Pokreni odmah</button>
${pauseBtn}
</span>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;padding:12px">
<div><div class="tb-s">Zadnji ciklus</div><b>${lc.fields_total != null ? lc.fields_total + ' polja' : '—'}</b>
<div class="tb-s">${lc.elapsed_s ? lc.elapsed_s + ' s' : ''}</div></div>
<div><div class="tb-s">Sportasi / Klubovi / Savezi</div><b>${(lc.sportas||0)+' / '+(lc.klub||0)+' / '+(lc.savez||0)}</b></div>
<div><div class="tb-s">Polja zadnjih 24h</div><b>${s.fields_24h||0}</b></div>
<div>
<div class="tb-s">Confidence prag: <b id="conf-val">${(conf).toFixed(2)}</b></div>
<input type="range" id="conf-slider" min="0" max="1" step="0.05" value="${conf}"
oninput="document.getElementById('conf-val').textContent=parseFloat(this.value).toFixed(2)"
onchange="setWorkerConfidence(parseFloat(this.value))" style="width:100%">
</div>
</div>
${recentRows ? `
<div style="border-top:1px solid var(--ln)">
<div style="padding:8px 12px;font-size:11px;color:var(--t4);text-transform:uppercase;letter-spacing:.5px">📋 Posljednji enrich-write zapisi</div>
<div class="tbl-wrap" style="max-height:240px;overflow:auto"><table style="width:100%">
<thead><tr><th>Vrijeme</th><th>Cilj</th><th>Polja</th><th>Izvor</th></tr></thead>
<tbody>${recentRows}</tbody>
</table></div>
</div>` : ''}
</div>
`;
// Auto-refresh every 10s while audit page is open
if(_enrichWorkerTimer) clearTimeout(_enrichWorkerTimer);
_enrichWorkerTimer = setTimeout(()=>{
if(_state.section === 'audit') loadEnrichWorker();
}, 10000);
}
async function setWorkerPause(paused){
try{
const r = await fetch(API+'/v2/enrich/worker/pause', {method:'POST',
headers:{'Content-Type':'application/json'}, body: JSON.stringify({paused})});
if(!r.ok) throw new Error('HTTP '+r.status);
toast(paused ? '⏸ Worker pauziran' : '▶️ Worker nastavlja', 'info', 2500);
loadEnrichWorker();
}catch(e){ toast('❌ '+(e.message||String(e)), 'error'); }
}
async function runWorkerNow(){
try{
const r = await fetch(API+'/v2/enrich/worker/run-now', {method:'POST'});
if(!r.ok) throw new Error('HTTP '+r.status);
toast('▶ Worker će pokrenuti novi ciklus odmah', 'info', 2500);
setTimeout(loadEnrichWorker, 2000);
}catch(e){ toast('❌ '+(e.message||String(e)), 'error'); }
}
async function setWorkerConfidence(value){
try{
const r = await fetch(API+'/v2/enrich/worker/confidence', {method:'POST',
headers:{'Content-Type':'application/json'}, body: JSON.stringify({value})});
if(!r.ok) throw new Error('HTTP '+r.status);
toast('🎚 Confidence prag: '+value.toFixed(2), 'info', 2000);
}catch(e){ toast('❌ '+(e.message||String(e)), 'error'); }
}
//=========== 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="ai-bar" style="display:flex;flex-direction:column;gap:8px;margin-bottom:12px;padding:12px;border:1px solid var(--bd);border-radius:8px;background:var(--bg2)">
<div style="display:flex;align-items:center;gap:8px">
<span style="font-size:14px">🤖</span>
<span style="font-weight:600;font-size:13px">DABI AI Copilot</span>
<span id="dash-ai-status" style="font-size:11px;color:var(--t2);margin-left:auto"></span>
</div>
<div style="display:flex;gap:6px">
<input id="dash-ai-q" type="text" placeholder="Pitaj DABI… (npr. Koliko klubova ima PGŽ?)"
onkeydown="if(event.key==='Enter'){event.preventDefault();dashAiAsk();}"
style="flex:1;padding:8px 10px;border:1px solid var(--bd);border-radius:6px;background:var(--bg);color:var(--t0);font-size:13px;outline:none">
<button onclick="dashAiAsk()" id="dash-ai-btn"
style="padding:8px 14px;border:0;border-radius:6px;background:var(--accent);color:#fff;font-size:13px;font-weight:600;cursor:pointer">Pitaj</button>
</div>
<div id="dash-ai-out" style="display:none;padding:10px;border-top:1px solid var(--bd);font-size:13px;line-height:1.5;white-space:pre-wrap;color:var(--t0);max-height:400px;overflow-y:auto"></div>
</div>
<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;flex-wrap:wrap">
<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="0">Sve godine</option>
</select>
<select id="dash-davatelj" 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="all" selected>Svi davatelji</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 dashAiAsk(){
const inp = document.getElementById('dash-ai-q');
const out = document.getElementById('dash-ai-out');
const btn = document.getElementById('dash-ai-btn');
const stat = document.getElementById('dash-ai-status');
if(!inp || !out || !btn) return;
const q = (inp.value||'').trim();
if(!q) return;
const tok = localStorage.getItem('pgz_access') || sessionStorage.getItem('pgz_access') || localStorage.getItem('access_token') || '';
if(!tok){
out.style.display='block';
out.textContent = '⚠ Prijava potrebna. Idi na /login pa se vrati ovamo.';
if(stat) stat.textContent = 'traži prijavu';
return;
}
btn.disabled = true; btn.textContent = '…';
if(stat) stat.textContent = 'razmišljam…';
out.style.display='block';
out.textContent = '⏳ DABI razmišlja…';
try{
const headers = {'Content-Type':'application/json','Authorization':'Bearer '+tok};
const r = await fetch(API+'/v2/ai/ask', {method:'POST', headers, body: JSON.stringify({question:q, query:q, q:q})});
if(r.status===401){ out.textContent = '⚠ Sesija je istekla. Idi na /login.'; if(stat) stat.textContent='401'; return; }
if(!r.ok){ const t = await r.text().catch(()=>''); out.textContent = '❌ Greška: HTTP '+r.status+(t?' — '+t.slice(0,200):''); if(stat) stat.textContent='greška'; return; }
const data = await r.json();
const answer = data.answer || data.response || data.text || JSON.stringify(data, null, 2).slice(0,1200);
out.textContent = answer;
if(stat) stat.textContent = 'odgovor spreman';
}catch(e){ out.textContent = '❌ '+(e.message||String(e)); if(stat) stat.textContent='greška'; }
finally{ btn.disabled = false; btn.textContent = 'Pitaj'; }
}
async function refreshDashNositelji(){
const selG = $('#dash-god');
const selD = $('#dash-davatelj');
if(!selG) return;
const god = selG.value;
const dav = selD ? selD.value : 'all';
const lbl = (god === '0' || Number(god) <= 0) ? 'sve godine' : god;
const out = $('#dash-nos-out');
out.innerHTML = '<div class="loading">Učitavanje primatelja '+lbl+'…</div>';
const url = '/dashboard/top-primatelji?godina='+god+(dav!=='all' ? '&davatelj='+encodeURIComponent(dav) : '')+'&limit=100';
const d = await api(url);
if(!d){ out.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
// Populate dropdowns dynamically (first time)
if(d.available_years && selG.options.length <= 1){
selG.innerHTML = '<option value="0">Sve godine</option>' + d.available_years.map(y =>
'<option value="'+y.godina+'"'+(String(y.godina)===god?' selected':'')+'>'+y.godina+' ('+y.broj+' / '+fmtEur(y.suma||0)+')</option>'
).join('');
}
if(d.available_davatelji && selD && selD.options.length <= 1){
selD.innerHTML = '<option value="all">Svi davatelji</option>' + d.available_davatelji.map(dn =>
'<option value="'+dn+'"'+(dn===dav?' selected':'')+'>'+dn+'</option>'
).join('');
}
const rows = (d.rows || []);
$('#dash-nos-cnt').textContent = rows.length+' primatelja · ukupno '+fmtEur((d.summary && d.summary.total_amount)||d.ukupno||0);
if(rows.length === 0){
out.innerHTML = '<div class="empty">Nema podataka za '+lbl+'</div>';
return;
}
out.innerHTML = `<div style="overflow-x:auto;max-height:520px;overflow-y:auto"><table>
<thead><tr><th>#</th><th>Korisnik</th><th>Sport</th><th>Vrsta</th><th class="num">Iznos</th><th>Platitelj</th><th>PDF</th></tr></thead>
<tbody>${rows.map((r,i) => {
const proxy = {
korisnik: r.naziv_kluba,
sport: r.sport && r.sport!=='n/a' ? r.sport : null,
vrsta: r.vrsta,
iznos_eur: r.iznos,
godina: r.godina,
izvor: r.davatelj,
napomena: r.napomena,
source_url: r.pdf_url,
klub_id: r.klub_id
};
const pjson = JSON.stringify(proxy).replace(/'/g,"&#39;");
const naziv_short = (r.naziv_kluba || '').length > 60 ? r.naziv_kluba.slice(0,57)+'...' : (r.naziv_kluba || '');
return `
<tr onclick='openPrimateljDetail(${pjson})' title="${esc(r.naziv_kluba || '')}">
<td>${i+1}</td>
<td><b>${esc(naziv_short)}</b>${r.godina && god==='0' ? ' <span class="tb-s">('+r.godina+')</span>' : ''}</td>
<td>${r.sport && r.sport!=='n/a' ? esc(r.sport) : '—'}</td>
<td>${esc(r.vrsta||'')}</td>
<td class="num"><b>${fmtEurFull(r.iznos)}</b></td>
<td>${esc(r.davatelj_naziv||'')}</td>
<td>${r.pdf_url?'<a href="'+esc(r.pdf_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>';
// BUG-E (2026-05-05): explicit filter — when financirani=true → priority-sort?only=true
const f = _filters.savezi;
const useOnly = f.financirani || window._pgz_filter_priority;
const url = useOnly
? '/v2/savezi/priority-sort?only=true&limit=500'
: '/v2/savezi/priority-sort?only=false&limit=500';
const d = await api(url);
if(!d){ root.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
_cache.savezi = d.rows || [];
_filters.savezi.total = (d.rows||[]).length;
}
renderSaveziShell();
applySaveziFilter();
}
function renderSaveziShell(){
const root = $('#pg-savezi');
const sports = Array.from(new Set((_cache.savezi||[]).map(s=>s.sport).filter(Boolean))).sort();
root.innerHTML = `
${_filtersBar('savezi')}
<div class="toolbar">
<input type="search" id="sav-q" placeholder="🔍 Pretraži savez…">
<select id="sav-sport"><option value="">Svi sportovi</option>${sports.map(s=>'<option value="'+esc(s)+'">'+esc(s)+'</option>').join('')}</select>
<select id="sav-kat">
<option value="">Sve razine</option>
<option value="zupanijski">Županijski</option>
<option value="gradski">Gradski</option>
</select>
<select id="sav-pgz">
<option value="">Svi savezi</option>
<option value="1">Samo PGŽ relevantni</option>
</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>
${window.renderPGZToggleBtn ? window.renderPGZToggleBtn('savezi') : ''}
<span class="tb-s" id="sav-cnt"></span>
<button id="sav-export-btn" class="export-btn" type="button">Export ▾</button>
</div>
<div id="sav-out"></div>
`;
$('#sav-q').addEventListener('input', debounce(applySaveziFilter, 200));
$('#sav-sport').addEventListener('change', applySaveziFilter);
$('#sav-kat').addEventListener('change', applySaveziFilter);
$('#sav-pgz').addEventListener('change', applySaveziFilter);
// Export ▾ — uses same /v2/savezi/priority-sort URL as the table loader.
if (window.attachExportDropdown) {
window.attachExportDropdown(
document.getElementById('sav-export-btn'),
function(){
const f = _filters.savezi || {};
const useOnly = f.financirani || window._pgz_filter_priority;
return '/sport/api' + (useOnly
? '/v2/savezi/priority-sort?only=true&limit=500'
: '/v2/savezi/priority-sort?only=false&limit=500');
},
'savezi'
);
}
}
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 : '';
const fSport = $('#sav-sport') ? $('#sav-sport').value : '';
const fKat = $('#sav-kat') ? $('#sav-kat').value : '';
let rows = _cache.savezi || [];
if(q) rows = rows.filter(s => (s.naziv||'').toLowerCase().includes(q) || (s.sport||'').toLowerCase().includes(q));
if(fSport) rows = rows.filter(s => (s.sport||'')===fSport);
if(fKat==='zupanijski') rows = rows.filter(s => /(?:zupanij|županij)/i.test(s.razina||''));
else if(fKat==='gradski') rows = rows.filter(s => /gradsk/i.test(s.razina||''));
if(pgz==='1') rows = rows.filter(s => s.pgz_relevant);
if(_sort.savezi) rows = sortRows(rows, _sort.savezi.key, _sort.savezi.dir);
_filtersUpdateCount('savezi', rows.length);
$('#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">${(window.pgzBadgePrefix?window.pgzBadgePrefix(s,'savez'):'')}${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>${(window.pgzBadgePrefix?window.pgzBadgePrefix(s,'savez'):'')}${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">${s.oib?formatOib(s.oib,{savez_id:s.id}):'—'}</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="panelDrill(openKlub, ${k.id})">
<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 ===========
// (legacy _klubFilters helpers replaced by global togglePGZFilter — SUB6)
async function loadKlubovi(){
const root = $('#pg-klubovi');
if(!_cache.klubovi){
root.innerHTML = '<div class="loading">Učitavanje klubova…</div>';
// RUSH-1 (2026-05-05): /api/klubovi URL built from _filters.klubovi state.
// Spec (CC_FINAL_RUSH slika 4) — 3 checkboxes:
// ☑ Samo financirani (PGŽ + RSS + Grad Rijeka) — single combined
// ☑ U godišnjaku
// ☐ Ima HNS roster
// Backend `financiran=true` is OR of all 3 davateljs (single source of truth
// = v_klubovi_financiranje view). Default = priority (fin OR godišnjak).
// Sort: ukupno_potpora DESC.
const f = _filters.klubovi;
const qs = new URLSearchParams();
qs.set('limit','2500');
qs.set('sort','potpora'); qs.set('order','desc'); // ukupno_potpora DESC NULLS LAST
if(f.financirani && f.godisnjak){
qs.set('kategorija','priority'); // OR semantics → priority = financiran OR godišnjak
} else if(f.financirani){
qs.set('financiran','true');
} else if(f.godisnjak){
qs.set('godisnjak','true');
}
if(f.hns_roster) qs.set('samo_hns_roster','true');
// Legacy global toggle still respected (if user clicks the old PGŽ button).
if(window._pgz_filter_priority && !qs.has('kategorija')) qs.set('kategorija','priority');
const d = await api('/klubovi?'+qs.toString());
if(!d){ root.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
_cache.klubovi = d.rows || [];
_filters.klubovi.total = (d.rows||[]).length;
}
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 = `
${_filtersBar('klubovi')}
<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>
<select id="kl-kat" title="Kategorija">
<option value="">Sve kategorije</option>
<option value="priority">Samo PGŽ priority</option>
<option value="financiran">Samo financirani</option>
<option value="godisnjak">Samo u godišnjaku</option>
</select>
<label><input type="checkbox" id="kl-nk"> Nositelj kvalitete</label>
<div class="toggle">
<button id="kl-card" class="${_state.viewKlubovi==='card'?'active':''}" onclick="setKluboviView('card')">Kartice</button>
<button id="kl-table" class="${_state.viewKlubovi==='table'?'active':''}" onclick="setKluboviView('table')">Tablica</button>
</div>
<button class="btn" onclick="enrichBulk('klub', 50, 70)">✨ Obogati (50)</button>
${window.renderPGZToggleBtn ? window.renderPGZToggleBtn('klubovi') : ''}
<span class="tb-s" id="kl-cnt"></span>
<button id="kl-export-btn" class="export-btn" type="button">Export ▾</button>
</div>
<div id="kl-out"></div>
`;
$('#kl-q').addEventListener('input', debounce(applyKluboviFilter, 200));
$('#kl-sport').addEventListener('change', applyKluboviFilter);
$('#kl-grad').addEventListener('change', applyKluboviFilter);
$('#kl-kat').addEventListener('change', applyKluboviFilter);
$('#kl-nk').addEventListener('change', applyKluboviFilter);
// Export ▾ — rebuilds the same querystring that loadKlubovi uses.
if (window.attachExportDropdown) {
window.attachExportDropdown(
document.getElementById('kl-export-btn'),
function(){
const f = _filters.klubovi || {};
const qs = new URLSearchParams();
qs.set('limit','2500');
qs.set('sort','potpora'); qs.set('order','desc');
if(f.financirani && f.godisnjak) qs.set('kategorija','priority');
else if(f.financirani) qs.set('financiran','true');
else if(f.godisnjak) qs.set('godisnjak','true');
if(f.hns_roster) qs.set('samo_hns_roster','true');
if(window._pgz_filter_priority && !qs.has('kategorija')) qs.set('kategorija','priority');
return '/sport/api/klubovi?'+qs.toString();
},
'klubovi'
);
}
}
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 kat = $('#kl-kat') ? $('#kl-kat').value : '';
const nk = $('#kl-nk') ? $('#kl-nk').checked : false;
let rows = _cache.klubovi || [];
if(q) rows = rows.filter(k => (k.klub||'').toLowerCase().includes(q) || (k.sport||'').toLowerCase().includes(q));
if(sport) rows = rows.filter(k => k.sport===sport);
if(grad) rows = rows.filter(k => k.grad===grad);
if(nk) rows = rows.filter(k => k.nositelj_kvalitete);
if(kat==='priority') rows = rows.filter(k => k.priority);
else if(kat==='financiran') rows = rows.filter(k => k.financiran);
else if(kat==='godisnjak') rows = rows.filter(k => k.godisnjak);
if(_sort.klubovi) rows = sortRows(rows, _sort.klubovi.key, _sort.klubovi.dir);
// BUG-E: live count + total
_filtersUpdateCount('klubovi', rows.length);
$('#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>');
}
// Wire tickbox-all & individual checks (no-op if absent)
const all = $('#kl-all');
if(all){
all.addEventListener('change', () => {
$$('.kl-pick').forEach(cb => { cb.checked = all.checked; });
}, {once:true});
}
}
function renderKluboviGrid(rows){
if(!rows.length) return '<div class="empty">Nema rezultata</div>';
return '<div class="grid-club">'+rows.map(k => {
const finTitle = [k.prima_pgz?'PGŽ':null, k.prima_rss?'RSS':null, k.prima_grad_rijeka?'Grad Rijeka':null].filter(Boolean).join(' + ') || 'financiran';
const potpora = (k.ukupno_potpora!=null) ? ' <b style="color:var(--pgz-gold)" title="ukupno potpora">'+fmtEur(k.ukupno_potpora)+'</b>' : '';
return `
<div class="entity" onclick="openKlub(${k.id})">
${k.priority?'<div class="et-tag" style="background:#ffd700;color:#1a1a1a">★ PRIO</div>':(k.nositelj_kvalitete?'<div class="et-tag">N.K.</div>':'')}
<div class="et">${(window.pgzBadgePrefix?window.pgzBadgePrefix(k,'klub'):'')}${esc(k.klub||k.sport||'(bez naziva)')}</div>
<div class="es">${txt(k.razina,'')} · ${txt(k.grad,'—')}</div>
<div class="em">
${k.financiran?'<span class="tag gd" title="'+esc(finTitle)+'">€</span>':''}
${k.godisnjak?'<span class="tag b" title="U godišnjaku">G</span>':''}
${potpora}
<span><b>${fmtNum(k.registriranih)}</b> reg.</span>
<span><b>${fmtNum(k.trenera)}</b> trenera</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><th style="width:34px"><input type="checkbox" id="kl-all" title="Označi sve"></th><th title="PGŽ priority">★</th>${sortHeader('klubovi','klub','Klub','')}${sortHeader('klubovi','sport','Sport','')}${sortHeader('klubovi','razina','Razina','')}${sortHeader('klubovi','grad','Grad','')}${sortHeader('klubovi','ukupno_potpora','Potpora','num')}${sortHeader('klubovi','registriranih','Reg.','num')}${sortHeader('klubovi','nositelj_kvalitete','Status','')}</tr></thead>
<tbody>${rows.map(k => {
const finTitle = [k.prima_pgz?'PGŽ':null, k.prima_rss?'RSS':null, k.prima_grad_rijeka?'Grad Rijeka':null].filter(Boolean).join(' + ') || 'financiran';
return `
<tr>
<td onclick="event.stopPropagation()"><input type="checkbox" class="kl-pick" data-id="${k.id}"></td>
<td onclick="openKlub(${k.id})">${k.priority?'<span class="tag gd" title="financiran ili u godišnjaku">★</span>':''}</td>
<td onclick="openKlub(${k.id})"><b>${(window.pgzBadgePrefix?window.pgzBadgePrefix(k,'klub'):'')}${esc(k.klub||k.sport||'(bez naziva)')}</b></td>
<td onclick="openKlub(${k.id})">${txt(k.sport)}</td>
<td onclick="openKlub(${k.id})">${txt(k.razina)}</td>
<td onclick="openKlub(${k.id})">${txt(k.grad)}</td>
<td onclick="openKlub(${k.id})" class="num"><b style="color:var(--pgz-gold)">${k.ukupno_potpora!=null?fmtEur(k.ukupno_potpora):'—'}</b></td>
<td onclick="openKlub(${k.id})" class="num">${fmtNum(k.registriranih)}</td>
<td onclick="openKlub(${k.id})">${k.financiran?'<span class="tag gd" title="'+esc(finTitle)+'">€</span>':''}${k.godisnjak?'<span class="tag b" title="godišnjak">G</span>':''}${k.nositelj_kvalitete?'<span class="tag gd">N.K.</span>':''}${k.aktivan?'<span class="tag gr">AKT</span>':'<span class="tag rd">NK</span>'}</td>
</tr>`;
}).join('')}</tbody>
</table></div>`;
}
// ─── Sport-aware enrichment helper (cached) ───
const _enrichSrcCache = {};
async function enrichSourceFor(sport){
if(!sport) return null;
const k = (sport||'').toLowerCase();
if(_enrichSrcCache[k]) return _enrichSrcCache[k];
try{
const d = await api('/v2/enrich-sources?sport='+encodeURIComponent(sport));
if(d && d.match){ _enrichSrcCache[k] = d.match; return d.match; }
} catch(e){}
return null;
}
async function openEnrichSourceForKlub(sport, naziv){
const src = await enrichSourceFor(sport);
if(!src){ window.toast && window.toast('Nema definiranog izvora za sport: '+(sport||'?'), 'warn', 3000); return; }
const base = (src.base_url||'').replace(/\/$/,'');
let url;
if((src.sport||'').toLowerCase() === 'nogomet'){
url = base + '/klubovi?q=' + encodeURIComponent(naziv||'');
} else {
url = base + '/?s=' + encodeURIComponent(naziv||'');
}
window.open(url, '_blank', 'noopener');
}
// ─── Export of selected klubovi ───
async function exportKlubovi(format){
const ids = $$('.kl-pick').filter(cb => cb.checked).map(cb => parseInt(cb.dataset.id, 10)).filter(Boolean);
if(!ids.length){ window.toast && window.toast('Označite najmanje jedan klub (checkbox)', 'warn', 3000); return; }
try{
const resp = await fetch(API + '/v2/export/klubovi', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ids, format})
});
if(!resp.ok){ window.toast && window.toast('Export greška: HTTP '+resp.status, 'error', 4000); return; }
const blob = await resp.blob();
const cd = resp.headers.get('Content-Disposition') || '';
const m = cd.match(/filename="?([^"]+)"?/);
const fname = m ? m[1] : ('pgz_klubovi.' + format);
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = fname;
document.body.appendChild(a); a.click(); a.remove();
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
window.toast && window.toast('Export gotov: '+ids.length+' klubova → '+format.toUpperCase(), 'success', 3000);
} catch(e){ window.toast && window.toast('Export greška: '+(e.message||e), 'error', 4000); }
}
async function openGodisnjak(godina){
// Drill-down panel za jedan godišnjak (PDF + popis spomenutih sportaša).
// Endpoint: /api/v2/godisnjak/{godina}/sportasi (vraća count + sportasi[])
// PDF: /api/v2/dokumenti/godisnjak/{godina}
godina = parseInt(godina, 10);
if(!godina) return;
openPanel('📚 Godišnjak '+godina, '<div class="loading">Učitavanje godišnjaka…</div>');
const d = await api('/v2/godisnjak/'+godina+'/sportasi?limit=500');
if(!d || d.detail){
openPanel('📚 Godišnjak '+godina, '<div class="empty">Godišnjak '+godina+' nije pronađen.</div>');
return;
}
const sportasi = d.sportasi || [];
const cnt = sportasi.length;
const pdfHref = '/sport/api/v2/dokumenti/godisnjak/'+godina;
// distinct sports for filter chips
const sports = Array.from(new Set(sportasi.map(s => s.sport).filter(Boolean))).sort((a,b)=>a.localeCompare(b,'hr'));
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)">Sportski godišnjak ${godina}</div>
<div style="font-size:12px;color:var(--t2);margin-top:4px">${fmtNum(cnt)} sportaša spomenuto</div>
</div>
<a href="${pdfHref}" target="_blank" rel="noopener"
class="btn" style="background:var(--accent);color:#fff;padding:6px 12px;border-radius:6px;font-weight:600;font-size:12px;text-decoration:none">
📄 Otvori PDF ↗
</a>
</div>
${sports.length ? `<div style="margin-bottom:10px;display:flex;flex-wrap:wrap;gap:4px;align-items:center">
<span style="font-size:11px;color:var(--t2)">Sport:</span>
<button class="tag" data-sport="" onclick="window._godSportFilter('${godina}','')" style="cursor:pointer">svi</button>
${sports.map(sp=>`<button class="tag" data-sport="${esc(sp)}" onclick="window._godSportFilter('${godina}',this.dataset.sport)" style="cursor:pointer">${esc(sp)}</button>`).join('')}
</div>` : ''}
<div id="god-${godina}-list" style="overflow-x:auto"><table>
<thead><tr><th>Sportaš</th><th>Sport</th><th>Klub</th><th style="text-align:center">🥇</th><th style="text-align:center">📊</th><th>Ključne riječi</th></tr></thead>
<tbody>${sportasi.map(s => `
<tr onclick="panelDrill(openSportas,${s.clan_id})" style="cursor:pointer" data-sport="${esc(s.sport||'')}">
<td><b>${esc((s.ime||'')+' '+(s.prezime||''))}</b></td>
<td>${esc(s.sport||'—')}</td>
<td>${esc(s.klub||'—')}</td>
<td style="text-align:center">${s.has_medal ? '<span class="tag gd" title="Medalja">🥇</span>' : ''}</td>
<td style="text-align:center">${s.has_kategorija ? '<span class="tag b" title="Kategorija">K</span>' : ''}</td>
<td>${(s.keywords||[]).slice(0,4).map(k=>'<span class="tag" style="font-size:10px">'+esc(k)+'</span>').join(' ')}</td>
</tr>`).join('')}
</tbody>
</table></div>
`;
openPanel('📚 Godišnjak '+godina, html);
}
// Sport filter helper (column index 1 = "Sport")
window._godSportFilter = function(godina, sp){
const tbody = document.querySelector('#god-'+godina+'-list tbody');
if(!tbody) return;
tbody.querySelectorAll('tr').forEach(tr => {
tr.style.display = (!sp || tr.dataset.sport === sp) ? '' : 'none';
});
};
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')">Roster (${clanovi.length})</div>
<div class="tab" onclick="switchKlubTab(this,'k-kat')">Kategorije</div>
${(k.sport||'').toLowerCase()==='nogomet' ? '<div class="tab" onclick="switchKlubTab(this,\'k-hns\')">HNS karijera</div>' : ''}
<div class="tab" onclick="switchKlubTab(this,'k-pot')">Potpore (${potpore.length})</div>
</div>
<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">${k.oib?formatOib(k.oib,{klub_id:k.id,savez_id:k.savez_id}):'—'}</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>
<div style="margin-top:14px;display:flex;gap:8px;flex-wrap:wrap">
<a class="btn primary" onclick="switchKlubTab(document.querySelector('.tab[onclick*=k-clan]'),'k-clan')" style="cursor:pointer;display:inline-flex;align-items:center;gap:6px">
👥 Vidi sportaše ovog kluba (${clanovi.length})
</a>
${(k.web||k.web_stranica) ? '<a class="btn" href="'+esc(k.web||k.web_stranica)+'" target="_blank" style="display:inline-flex;align-items:center;gap:6px">🌐 Službena stranica</a>' : ''}
<button class="btn" onclick="openEnrichSourceForKlub(${JSON.stringify(k.sport||'')}, ${JSON.stringify(k.naziv||'')})" style="display:inline-flex;align-items:center;gap:6px">🌐 Obogati podatke (sport-savez)</button>
</div>
${k.napomena ? '<div class="card" style="margin-top:14px"><div class="card-t" style="margin-bottom:6px">Napomena</div><div style="font-size:12px;color:var(--t1);line-height:1.5">'+esc(k.napomena)+'</div></div>' : ''}
</div>
<div id="k-clan" class="ktab" style="display:none">
${clanovi.length ? `<div style="overflow-x:auto;max-height:500px;overflow-y:auto"><table>
<thead><tr><th>Sportaš</th><th>Spol</th><th>Pozicija</th><th>Kategorija</th><th>Tagovi</th></tr></thead>
<tbody>${clanovi.map(c => `
<tr onclick="panelDrill(openSportas, ${c.id})">
<td><b>${esc(c.ime||'')} ${esc(c.prezime||'')}</b></td>
<td>${txt(c.spol)}</td>
<td>${txt(c.pozicija)}</td>
<td>${txt(c.kategorija)}</td>
<td>${c.reprezentativac?'<span class="tag gd">REPR</span>':''}${c.kategoriziran?'<span class="tag b">KAT</span>':''}${c.stipendiran?'<span class="tag gr">STIP</span>':''}</td>
</tr>`).join('')}
</tbody>
</table></div>` : '<div class="empty">Nema podataka o sportašima</div>'}
</div>
<div id="k-kat" class="ktab" style="display:none">
${(() => {
if(!clanovi.length) return '<div class="empty">Nema podataka za grupiranje</div>';
const groups = {};
clanovi.forEach(c => {
const cats = (c.kategorije && c.kategorije.length) ? c.kategorije : [c.kategorija || '(nepoznata)'];
cats.forEach(kat => {
const key = kat || '(nepoznata)';
(groups[key] = groups[key] || []).push(c);
});
});
return Object.keys(groups).sort().map(kat => `
<details style="margin-bottom:8px" ${groups[kat].length<=12?'open':''}>
<summary style="cursor:pointer;padding:8px;background:rgba(255,255,255,.04);border-radius:6px"><b>${esc(kat)}</b> · ${groups[kat].length} igrač${groups[kat].length===1?'':'a'}</summary>
<table style="margin-top:6px"><tbody>${groups[kat].map(c => `
<tr onclick="panelDrill(openSportas, ${c.id})">
<td><b>${esc(c.ime||'')} ${esc(c.prezime||'')}</b></td>
<td>${txt(c.spol)}</td>
<td>${txt(c.pozicija)}</td>
</tr>`).join('')}</tbody></table>
</details>`).join('');
})()}
</div>
${(k.sport||'').toLowerCase()==='nogomet' ? `
<div id="k-hns" class="ktab" style="display:none">
<div class="card" style="padding:14px">
<div class="card-t" style="margin-bottom:8px">⚽ HNS Semafor karijera</div>
<div style="font-size:13px;color:var(--t1);line-height:1.6">
Klikni na pojedinog igrača (tab "Roster") za detalje sezona, golova i utakmica iz HNS Semafora.
${(k.naziv && k.naziv.match(/HNK|NK |GNK|RNK/i)) ? '<br><br><b>Tip:</b> Ovaj klub vjerojatno postoji na HNS Semaforu.' : ''}
</div>
<div style="margin-top:14px">
<button class="btn" onclick="openEnrichSourceForKlub('nogomet', ${JSON.stringify(k.naziv||'')})">🌐 Otvori HNS Semafor pretragu</button>
</div>
</div>
</div>` : ''}
<div id="k-pot" class="ktab" style="display:none">
${potpore.length ? `<div style="overflow-x:auto"><table>
<thead><tr><th>Godina</th><th>Naziv</th><th class="num">Iznos</th></tr></thead>
<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';
}
//=========== IGRAČI PO KATEGORIJI ===========
async function loadIgraciKat(){
const root = $('#pg-igraci-kat');
root.innerHTML = `
<div class="toolbar">
<select id="ik-sport"><option value="">Svi sportovi</option></select>
<select id="ik-klub"><option value="">Svi klubovi</option></select>
<button class="btn primary" onclick="reloadIgraciKat()">↻ Osvježi</button>
<span class="tb-s" id="ik-cnt"></span>
</div>
<div id="ik-out"><div class="loading">Učitavanje…</div></div>
`;
// Populate sport dropdown from already-cached klubovi if present
const sports = Array.from(new Set((_cache.klubovi||[]).map(k=>k.sport).filter(Boolean))).sort();
const ss = $('#ik-sport');
sports.forEach(s => { const o=document.createElement('option'); o.value=s; o.textContent=s; ss.appendChild(o); });
const klubovi = (_cache.klubovi||[]).map(k => [k.id, k.klub||k.naziv]).sort((a,b)=>String(a[1]).localeCompare(String(b[1]),'hr'));
const sk = $('#ik-klub');
klubovi.slice(0,400).forEach(([id,n]) => { const o=document.createElement('option'); o.value=id; o.textContent=n; sk.appendChild(o); });
$('#ik-sport').addEventListener('change', reloadIgraciKat);
$('#ik-klub').addEventListener('change', reloadIgraciKat);
reloadIgraciKat();
}
async function reloadIgraciKat(){
const sport = $('#ik-sport') ? $('#ik-sport').value : '';
const klub = $('#ik-klub') ? $('#ik-klub').value : '';
const out = $('#ik-out');
out.innerHTML = '<div class="loading">Učitavanje…</div>';
let qs = '?limit_per_kat=200';
if(sport) qs += '&sport=' + encodeURIComponent(sport);
if(klub) qs += '&klub_id=' + encodeURIComponent(klub);
const d = await api('/v2/sportasi-by-kategorija' + qs);
if(!d || !d.groups){ out.innerHTML = '<div class="empty">Greška pri dohvatu</div>'; return; }
$('#ik-cnt').textContent = d.total_kategorija + ' kategorija';
if(!d.groups.length){ out.innerHTML = '<div class="empty">Nema rezultata</div>'; return; }
out.innerHTML = d.groups.map(g => `
<details style="margin-bottom:10px" ${g.count<=12?'open':''}>
<summary style="cursor:pointer;padding:10px;background:rgba(255,255,255,.04);border-radius:8px;font-size:14px"><b>${esc(g.kategorija)}</b> · ${g.count} igrač${g.count===1?'':'a'}</summary>
<div class="card" style="padding:0;margin-top:6px;overflow-x:auto"><table>
<thead><tr><th>Igrač</th><th>Klub</th><th>Sport</th><th>Pozicija</th><th>Godina rođ.</th><th>Tagovi</th></tr></thead>
<tbody>${(g.rows||[]).map(c => `
<tr onclick="openSportas(${c.id})">
<td><b>${esc((c.ime||'')+' '+(c.prezime||''))}</b></td>
<td>${txt(c.klub_naziv)}</td>
<td>${txt(c.sport)}</td>
<td>${txt(c.pozicija)}</td>
<td>${c.datum_rodenja ? String(c.datum_rodenja).slice(0,4) : '—'}</td>
<td>${c.reprezentativac?'<span class="tag gd">REPR</span>':''}${c.kategoriziran?'<span class="tag b">KAT</span>':''}${c.stipendiran?'<span class="tag gr">STIP</span>':''}</td>
</tr>`).join('')}</tbody>
</table></div>
</details>
`).join('');
}
function applyIgraciKatFilter(){ /* hook for setSort; igraci-kat re-renders via reloadIgraciKat */ }
//=========== SPORTAŠI ===========
async function loadSportasi(){
const root = $('#pg-sportasi');
if(!_cache.clanovi){
root.innerHTML = '<div class="loading">Učitavanje sportaša…</div>';
// BUG-E (2026-05-05): explicit filter via /v2/sportasi/filtered.
// Priority + HNS profil + godina_rod_od/do are server-side params.
const f = _filters.sportasi;
const useFiltered = f.priority || f.hns_profil || f.godina_od || f.godina_do;
let url;
if(useFiltered){
const qs = new URLSearchParams();
qs.set('limit','2000');
if(f.priority) qs.set('samo_priority','true');
if(f.hns_profil) qs.set('samo_s_hns','true');
if(f.godina_od) qs.set('godina_rod_od', String(f.godina_od));
if(f.godina_do) qs.set('godina_rod_do', String(f.godina_do));
url = '/v2/sportasi/filtered?'+qs.toString();
} else if(window._pgz_filter_priority){
url = '/v2/clanovi/priority-sort?only=true&limit=2000';
} else {
url = '/clanovi-full?limit=500';
}
const d = await api(url);
if(!d){ root.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
_cache.clanovi = d.rows || [];
_filters.sportasi.total = (d.rows||[]).length;
}
renderSportasiShell();
applySportasiFilter();
}
function renderSportasiShell(){
const root = $('#pg-sportasi');
const sports = Array.from(new Set((_cache.clanovi||[]).map(c=>c.sport).filter(Boolean))).sort();
const klubovi = Array.from(new Map((_cache.clanovi||[]).filter(c=>c.klub_id).map(c=>[c.klub_id, c.klub_naziv_godisnjak||c.klub_naziv||('Klub #'+c.klub_id)])).entries()).sort((a,b)=>String(a[1]).localeCompare(String(b[1]),'hr'));
const kats = Array.from(new Set((_cache.clanovi||[]).flatMap(c => (c.kategorije && c.kategorije.length ? c.kategorije : [c.kategorija]).filter(Boolean)))).sort();
root.innerHTML = `
${_filtersBar('sportasi')}
<div class="toolbar">
<input type="search" id="sp-q" placeholder="🔍 Ime ili prezime…">
<select id="sp-sport"><option value="">Svi sportovi</option>${sports.map(s=>'<option value="'+esc(s)+'">'+esc(s)+'</option>').join('')}</select>
<select id="sp-klub"><option value="">Svi klubovi</option>${klubovi.slice(0,400).map(([id,n])=>'<option value="'+id+'">'+esc(n)+'</option>').join('')}</select>
<select id="sp-kat"><option value="">Sve kategorije</option>${kats.map(k=>'<option value="'+esc(k)+'">'+esc(k)+'</option>').join('')}</select>
<input type="number" id="sp-god" placeholder="Godina rođ." min="1900" max="2030" style="width:120px">
<select id="sp-status">
<option value="">Svi statusi</option>
<option value="aktivan">Aktivni</option>
<option value="reprezentativac">Reprezentativci</option>
<option value="kategoriziran">Kategorizirani</option>
<option value="stipendiran">Stipendirani</option>
</select>
<select id="sp-hoo">
<option value="">Sve HOO kategorije</option>
<option value="1">I. kategorija</option>
<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>
${window.renderPGZToggleBtn ? window.renderPGZToggleBtn('sportasi') : ''}
<span class="tb-s" id="sp-cnt"></span>
<button class="btn" onclick="enrichBulk('sportas', 50, 70)">✨ Obogati sve (50)</button>
<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-sport').addEventListener('change', applySportasiFilter);
$('#sp-klub').addEventListener('change', applySportasiFilter);
$('#sp-kat').addEventListener('change', applySportasiFilter);
$('#sp-god').addEventListener('input', debounce(applySportasiFilter, 250));
$('#sp-status').addEventListener('change', applySportasiFilter);
$('#sp-hoo').addEventListener('change', applySportasiFilter);
$('#sp-rep').addEventListener('change', applySportasiFilter);
$('#sp-foto').addEventListener('change', applySportasiFilter);
}
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;
const fSport = $('#sp-sport') ? $('#sp-sport').value : '';
const fKlub = $('#sp-klub') ? $('#sp-klub').value : '';
const fKat = $('#sp-kat') ? $('#sp-kat').value : '';
const fGod = $('#sp-god') ? $('#sp-god').value.trim() : '';
const fStat = $('#sp-status') ? $('#sp-status').value : '';
let rows = _cache.clanovi || [];
if(q) rows = rows.filter(c => ((c.ime||'')+' '+(c.prezime||'')).toLowerCase().includes(q));
if(fSport) rows = rows.filter(c => (c.sport||'') === fSport);
if(fKlub) rows = rows.filter(c => String(c.klub_id||'') === String(fKlub));
if(fKat) rows = rows.filter(c => (c.kategorije && c.kategorije.includes(fKat)) || c.kategorija === fKat);
if(fGod) rows = rows.filter(c => String(c.datum_rodenja||c.datum_rodjenja||'').slice(0,4) === fGod);
if(fStat === 'aktivan') rows = rows.filter(c => c.aktivan);
else if(fStat === 'reprezentativac') rows = rows.filter(c => c.reprezentativac);
else if(fStat === 'kategoriziran') rows = rows.filter(c => c.kategoriziran);
else if(fStat === 'stipendiran') rows = rows.filter(c => c.stipendiran);
if(rep) rows = rows.filter(c => c.reprezentativac);
if(foto) rows = rows.filter(c => c.slika_url);
if(hoo) rows = rows.filter(c => String(c.hoo_kategorija||c.kategorija_hoo||'')===hoo);
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);
_filtersUpdateCount('sportasi', rows.length);
$('#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>';
}
// RUSH-2 (2026-05-05): avatarUrl + avatarHTML helpers. Small circular avatar
// to the left of the name in player cards (per Damir slika 6 spec).
// Author: Damir Radulić (dradulic@outlook.com / damir@rinet.one)
function avatarUrl(c){
if(!c) return null;
const u = c.slika_url || c.avatar || c.photo_url;
if(!u) return null;
if(/^https?:/i.test(u)) return u;
if(u.startsWith('/')) return u;
return '/sport/uploads/avatars/'+u;
}
function avatarHTML(c, sizePx){
const sz = sizePx || 36;
const initials = (((c.ime||'?')[0]||'?')+((c.prezime||'?')[0]||'?')).toUpperCase();
const url = avatarUrl(c);
if(url){
return '<span class="rush2-avatar" style="width:'+sz+'px;height:'+sz+'px;font-size:'+Math.round(sz*0.4)+'px"><img src="'+esc(url)+'" alt="" onerror="this.style.display=\'none\';this.parentElement.classList.add(\'r2a-fb\');this.parentElement.innerHTML=\''+initials+'\'"></span>';
}
return '<span class="rush2-avatar r2a-fb" style="width:'+sz+'px;height:'+sz+'px;font-size:'+Math.round(sz*0.4)+'px">'+initials+'</span>';
}
function buildPlayerCard(c){
const initials = (((c.ime||'?')[0]||'?')+((c.prezime||'?')[0]||'?')).toUpperCase();
const photoSrc = avatarUrl(c) || c.slika_url;
const photo = photoSrc ? '<img src="'+esc(photoSrc)+'" 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;
const smallAv = avatarHTML(c, 32);
return `
<div class="player-card" onclick="openSportas(${c.id})">
<div class="ph">${photo}</div>
<div class="pb">
<div class="pn-row">${smallAv}<div class="pn">${(window.pgzBadgePrefix?window.pgzBadgePrefix(c,'sportas'):'')}${esc(c.ime||'')} ${esc(c.prezime||'')}</div></div>
<div class="pp">${txt(c.sport,'—')} · ${txt(c.pozicija,'')}</div>
<div class="pk">${txt(c.klub_naziv_godisnjak||c.klub_naziv,'')}</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>${(window.pgzBadgePrefix?window.pgzBadgePrefix(c,'sportas'):'')}${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>');
// Pull primary profile + (optionally) richer v2 dossier in parallel.
// /sportas/{id}/profil = sezone+utakmice (primary). /v2/clan/{id}/full = canonical multi-sport fields.
const [dRaw, dV2] = await Promise.all([
api('/sportas/'+id+'/profil'),
api('/v2/clan/'+id+'/full').catch(()=>null)
]);
if(!dRaw || dRaw.detail){
openPanel('Sportaš', '<div class="empty">Sportaš nije pronađen</div>');
return;
}
// Merge: primary wins, but pull missing height/weight/foot/biografija from v2 dossier
const d = Object.assign({}, dRaw);
if(dV2 && dV2.profile){
const p = dV2.profile;
['visina_cm','tezina_kg','dominantna_noga','broj_dresa','biografija','mjesto_rodenja','mjesto_rodjenja','datum_rodenja','datum_rodjenja','spol','sport','aktivan','pozicija','klub_naziv','klub_id','dob_age','hns_igrac_id','profile_url','reprezentativac','stipendiran','datum_pristupa','email','telefon','oib','slug','slika_url']
.forEach(k => { if((d[k]===null||d[k]===undefined||d[k]==='') && p[k]!==null && p[k]!==undefined && p[k]!=='') d[k]=p[k]; });
}
const stats = d.stats || {};
const sezone = (d.clan_sezona && d.clan_sezona.length) ? d.clan_sezona : ((dV2 && dV2.hns_seasons) || []);
const utakmice = (d.utakmice && d.utakmice.length) ? d.utakmice : ((dV2 && dV2.hns_matches) || []);
const nagrade = d.nagrade || [];
const godisnjaci = d.godisnjak_godine || d.godisnjaci || [];
// SUB6 2026-05-05: M2M kategorije (clan_kategorije) iz /v2/clan/{id}/full
const kategorije = (dV2 && dV2.kategorije) || [];
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;
// Build HNS deep link: prefer profile_url; otherwise compose /igraci/{hns_id}/{slug}/ from semafor when we have hns_igrac_id.
const slug = d.slug || ((d.ime||'')+'-'+(d.prezime||'')).toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g,'').replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'');
const hnsId = d.hns_igrac_id || d.source_id || (d.vanjski_id && d.vanjski_id.hns_semafor) || null;
const hnsUrl = d.profile_url || (hnsId ? ('https://semafor.hns.family/igraci/'+encodeURIComponent(hnsId)+'/'+encodeURIComponent(slug)+'/') : null);
const fullName = ((d.ime||'')+' '+(d.prezime||'')).trim();
const ggUrl = 'https://www.google.com/search?q='+encodeURIComponent(fullName+' nogometaš HR');
const wikiUrl = 'https://hr.wikipedia.org/w/index.php?search='+encodeURIComponent(fullName);
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="panelDrill(openKlub,'+d.klub_id+')"><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 class="pp-bio-row">
${d.visina_cm?'<span class="pp-bio-chip"><b>'+esc(d.visina_cm)+'</b>cm visina</span>':''}
${d.tezina_kg?'<span class="pp-bio-chip"><b>'+esc(d.tezina_kg)+'</b>kg težina</span>':''}
${d.dominantna_noga?'<span class="pp-bio-chip"><b>'+esc(d.dominantna_noga)+'</b>noga</span>':''}
${d.pozicija?'<span class="pp-bio-chip"><b>'+esc(d.pozicija)+'</b></span>':''}
${d.broj_dresa?'<span class="pp-bio-chip">#<b>'+esc(d.broj_dresa)+'</b></span>':''}
</div>
<!-- BUG-F (2026-05-05): external links moved to dedicated 🔗 Linkovi tab -->
</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>
<!-- BUG-F (2026-05-05) — 4 explicit tabs: Profil / HNS Karijera / Utakmice (last 30) / Linkovi -->
<div class="tabs">
<div class="tab active" onclick="switchPlayerTab(this,'p-prof')">👤 Profil</div>
<div class="tab" onclick="switchPlayerTab(this,'p-sez')">🏆 HNS Karijera (${sezone.length})</div>
<div class="tab" onclick="switchPlayerTab(this,'p-utak')">📅 Utakmice (poslj. ${Math.min(utakmice.length,30)})</div>
<div class="tab" onclick="switchPlayerTab(this,'p-link')">🔗 Linkovi</div>
</div>
<div id="p-sez" class="ptab" style="display:none">
<div class="pp-section-h">🏆 HNS Karijera <span class="cnt">${sezone.length} sezon${sezone.length===1?'a':(sezone.length<5&&sezone.length>1?'e':'a')}</span></div>
${sezone.length ? `<div style="overflow-x:auto"><table>
<thead><tr><th>Sezona</th><th>Klub</th><th>Natjecanje</th><th class="num">Nastupi</th><th class="num">Golovi</th><th class="num">Asis.</th><th class="num">Žuti</th><th class="num">Crv.</th><th class="num">Min.</th><th></th></tr></thead>
<tbody>${[...sezone].sort((a,b)=>String(b.sezona||'').localeCompare(String(a.sezona||''))).map(s => `
<tr class="no-click">
<td><b>${esc(s.sezona||'')}</b></td>
<td>${esc(s.klub_naziv||'')}</td>
<td>${esc(s.natjecanje||'')}</td>
<td class="num">${fmtNum(s.nastupi)}</td>
<td class="num"><b style="color:var(--pgz-gold)">${fmtNum(s.pogoci ?? s.golovi)}</b></td>
<td class="num">${fmtNum(s.asistencije)}</td>
<td class="num" style="color:var(--amber)">${fmtNum(s.zuti_kartoni ?? s.zuti)}</td>
<td class="num" style="color:var(--red)">${fmtNum(s.crveni_kartoni ?? s.crveni)}</td>
<td class="num">${fmtNum(s.minute)}</td>
<td>${(s.natjecanje_url||s.source_url)?'<a href="'+esc(s.natjecanje_url||s.source_url)+'" target="_blank">↗</a>':''}</td>
</tr>`).join('')}
</tbody>
</table></div>` : '<div class="empty">Nema sezonskih podataka iz HNS Semafora</div>'}
</div>
<div id="p-utak" class="ptab" style="display:none">
<div class="pp-section-h">📅 Utakmice <span class="cnt">posljednjih ${Math.min(utakmice.length,30)} ${utakmice.length===1?'utakmica':(utakmice.length<5&&utakmice.length>1?'utakmice':'utakmica')}</span></div>
${utakmice.length ? `<div style="overflow-x:auto;max-height:560px;overflow-y:auto"><table>
<thead><tr><th>Datum</th><th>Natjecanje</th><th colspan="3">Susret</th><th>Pozicija</th><th class="num">Min.</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>${utakmice.slice(0,30).map(u => {
const dom = u.klub_dom || u.domacin || '';
const gost = u.klub_gost || u.gost || '';
const golovi = (u.pogodaka != null ? u.pogodaka : u.golovi);
const minute = (u.minute != null) ? u.minute : ((u.minute_od != null && u.minute_do != null) ? (u.minute_do - u.minute_od) : null);
return `
<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(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(gost)}</td>
<td>${esc(u.pozicija||'—')}</td>
<td class="num">${fmtNum(minute)}</td>
<td class="num"><b style="color:var(--pgz-gold)">${fmtNum(golovi)}</b></td>
<td class="num">${fmtNum(u.asistencije)}</td>
<td class="num" style="color:var(--amber)">${fmtNum(u.zuti)}</td>
<td class="num" style="color:var(--red)">${fmtNum(u.crveni)}</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-prof" class="ptab">
<div class="pp-section-h">👤 Profil <span class="cnt">${esc(d.ime||'')} ${esc(d.prezime||'')}</span></div>
<!-- Top: name + dres + active club + HNS deep link -->
<div class="prof-top">
<div class="prof-name-block">
<div class="prof-name">${esc(d.ime||'')} ${esc(d.prezime||'')}</div>
<div class="prof-sub">
${dob?'📅 '+fmtDate(dob)+(d.dob_age?' · '+d.dob_age+' god.':(dob?(' · '+(new Date().getFullYear()-Number(String(dob).slice(0,4)))+' god.'):'')):'—'}
${(d.mjesto_rodjenja||d.mjesto_rodenja)?' · '+esc(d.mjesto_rodjenja||d.mjesto_rodenja):''}
</div>
<div class="prof-club">
${d.klub_id ? '<a class="link-chip" onclick="panelDrill(openKlub,'+d.klub_id+')">🏟️ '+esc(d.klub_naziv_full||d.klub_naziv||d.klub_naziv_godisnjak||'—')+'</a>' : '🏟️ '+esc(d.klub_naziv_full||d.klub_naziv||d.klub_naziv_godisnjak||'—')}
${d.aktivan?'<span class="tag gr" style="margin-left:8px">AKTIVAN</span>':'<span class="tag rd" style="margin-left:8px">NEAKTIVAN</span>'}
</div>
${hnsUrl?'<div style="margin-top:10px"><a class="pp-link hns" href="'+esc(hnsUrl)+'" target="_blank" rel="noopener">⚽ HNS Semafor profil ↗</a></div>':''}
</div>
${d.broj_dresa?'<div class="prof-dres"><div class="prof-dres-num">'+esc(d.broj_dresa)+'</div><div class="prof-dres-l">DRES</div></div>':''}
</div>
<!-- Bio grid -->
<div class="prof-grid">
<div class="prof-cell"><div class="l">Pozicija</div><div class="v">${txt(d.pozicija)}</div></div>
<div class="prof-cell"><div class="l">Dominantna noga</div><div class="v">${txt(d.dominantna_noga)}</div></div>
<div class="prof-cell"><div class="l">Visina</div><div class="v">${d.visina_cm?'<b>'+d.visina_cm+'</b> cm':'—'}</div></div>
<div class="prof-cell"><div class="l">Težina</div><div class="v">${d.tezina_kg?'<b>'+d.tezina_kg+'</b> kg':'—'}</div></div>
<div class="prof-cell"><div class="l">Datum rođenja</div><div class="v">${fmtDate(dob)}</div></div>
<div class="prof-cell"><div class="l">Spol</div><div class="v">${txt(d.spol)}</div></div>
<div class="prof-cell"><div class="l">OIB</div><div class="v">${d.oib?(canSeeFullOib({klub_id:d.klub_id,savez_id:d.savez_id})?'<a class="link-chip" onclick="openOIB(&quot;'+esc(d.oib)+'&quot;)">'+esc(d.oib)+'</a>':maskOib(d.oib)):'—'}</div></div>
<div class="prof-cell"><div class="l">Email</div><div class="v">${d.email?'<a href="mailto:'+esc(d.email)+'">'+esc(d.email)+'</a>':'—'}</div></div>
<div class="prof-cell"><div class="l">Telefon</div><div class="v">${txt(d.telefon)}</div></div>
<div class="prof-cell"><div class="l">HNS ID</div><div class="v">${hnsId?esc(hnsId):'—'}</div></div>
<div class="prof-cell"><div class="l">Datum pristupa</div><div class="v">${fmtDate(d.datum_pristupa)}</div></div>
<div class="prof-cell"><div class="l">Status</div><div class="v">${d.aktivan?'<span class="tag gr">AKTIVAN</span>':'<span class="tag rd">NEAKTIVAN</span>'}${d.reprezentativac?' <span class="tag gd">REPR</span>':''}${d.stipendiran?' <span class="tag am">STIP</span>':''}</div></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>' : ''}
${kategorije.length ? `<div class="pp-section-h" style="margin-top:18px">🏷️ Kategorije <span class="cnt">${kategorije.length}</span></div>
<div style="overflow-x:auto;max-height:260px;overflow-y:auto"><table>
<thead><tr><th>Sezona</th><th>Kategorija</th><th>Klub</th><th>Izvor</th><th></th></tr></thead>
<tbody>${kategorije.map(k => `
<tr class="no-click">
<td><b>${esc(k.sezona||'—')}</b></td>
<td><span class="tag b">${esc(k.kategorija||'—')}</span></td>
<td>${k.klub_id ? '<a class="link-chip" onclick="panelDrill(openKlub,'+k.klub_id+')">'+esc(k.klub_naziv||('Klub #'+k.klub_id))+'</a>' : (k.klub_naziv?esc(k.klub_naziv):'—')}</td>
<td>${esc(k.source||'—')}</td>
<td>${k.source_url?'<a href="'+esc(k.source_url)+'" target="_blank" rel="noopener">↗</a>':''}</td>
</tr>`).join('')}
</tbody>
</table></div>` : ''}
${godisnjaci.length ? `<div class="pp-section-h" style="margin-top:18px">📚 Godišnjaci <span class="cnt">${godisnjaci.length}</span></div>
<div class="kv">
<div class="k">Prvi godišnjak</div><div class="v">${(()=>{const _g=d.godisnjak_prvi||godisnjaci[0];return _g?'<a class="tag b" href="javascript:void(0)" onclick="openGodisnjak('+_g+');return false;" title="Otvori godišnjak '+esc(_g)+'">'+esc(_g)+'</a>':'—';})()}</div>
<div class="k">Zadnji godišnjak</div><div class="v">${(()=>{const _g=d.godisnjak_zadnji||godisnjaci[godisnjaci.length-1];return _g?'<a class="tag b" href="javascript:void(0)" onclick="openGodisnjak('+_g+');return false;" title="Otvori godišnjak '+esc(_g)+'">'+esc(_g)+'</a>':'—';})()}</div>
<div class="k">Sve godine</div><div class="v">${godisnjaci.map(g=>'<a class="tag b" href="javascript:void(0)" onclick="openGodisnjak('+g+');return false;" title="Otvori godišnjak '+esc(g)+'">'+esc(g)+'</a>').join(' ')}</div>
</div>` : ''}
${nagrade.length ? `<div class="pp-section-h" style="margin-top:18px">🏅 Nagrade <span class="cnt">${nagrade.length}</span></div>
<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>
<!-- BUG-F (2026-05-05) — 🔗 Linkovi tab: external profile lookups -->
<div id="p-link" class="ptab" style="display:none">
<div class="pp-section-h">🔗 Linkovi <span class="cnt">${esc(fullName||'sportaš')}</span></div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px;margin-top:8px">
${hnsUrl
? `<a class="pp-link hns" href="${esc(hnsUrl)}" target="_blank" rel="noopener" style="padding:18px;font-size:14px;justify-content:space-between"><span>⚽ HNS Semafor profil</span><span style="font-family:var(--mono);color:var(--t3);font-size:11px">#${esc(hnsId||'')} →</span></a>`
: `<div class="pp-link" style="padding:18px;font-size:14px;opacity:.5;cursor:not-allowed">⚽ HNS profil <span style="font-size:11px;color:var(--t3);margin-left:auto">nije povezan</span></div>`}
<a class="pp-link gg" href="${esc(ggUrl)}" target="_blank" rel="noopener" style="padding:18px;font-size:14px;justify-content:space-between"><span>🔍 Google pretraga</span><span style="color:var(--t3);font-size:11px">→</span></a>
<a class="pp-link wiki" href="${esc(wikiUrl)}" target="_blank" rel="noopener" style="padding:18px;font-size:14px;justify-content:space-between"><span>📖 Wikipedia</span><span style="color:var(--t3);font-size:11px">→</span></a>
</div>
${hnsId ? `<div style="margin-top:14px;padding:10px 12px;background:var(--bg2);border:1px solid var(--rim);border-radius:5px;font-size:11.5px;color:var(--t2);font-family:var(--mono)">HNS ID: <b style="color:var(--pgz-gold)">${esc(hnsId)}</b> · slug: <span style="color:var(--t1)">${esc(slug||'—')}</span></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');
// Učitaj meta options
let meta = {sportovi: [], vrste: [], davatelji: [], godine: []};
try { meta = await api('/v2/potpore/meta'); } catch(e) { console.warn('meta fail', e); }
root.innerHTML = `
<div class="toolbar" style="flex-wrap:wrap;gap:8px">
<select id="fi-god">
${meta.godine.map(g => `<option value="${g.godina}">${g.godina} (${g.broj}, ${fmtEur(g.suma||0)})</option>`).join('')}
</select>
<select id="fi-davatelj" title="Izvor">
<option value="all">Svi izvori</option>
<option value="pgz">🏛 PGŽ</option>
<option value="rss">🌊 RSS (Riječki sportski savez)</option>
<option value="grad_rijeka">🌆 Grad Rijeka</option>
</select>
<select id="fi-sport" title="Sport">
<option value="">Svi sportovi</option>
${meta.sportovi.map(s => `<option value="${s}">${s}</option>`).join('')}
</select>
<select id="fi-vrsta" title="Vrsta">
<option value="">Sve vrste</option>
${meta.vrste.map(v => `<option value="${v}">${v.replace('_',' ')}</option>`).join('')}
</select>
<input type="search" id="fi-q" placeholder="🔍 Pretraži korisnika…">
<label class="tb-s" style="display:flex;align-items:center;gap:6px;cursor:pointer;user-select:none" title="Samo plaćanja koja imaju vezu na realni klub (klub_id IS NOT NULL). Isključuje skrečane PDF section-headere.">
<input type="checkbox" id="fi-samo-klubovi" checked style="cursor:pointer"> samo klubovi
</label>
<button id="fi-manual-btn" class="btn sm" style="background:var(--pgz-gold,#c5a040);color:#000" title="Dodaj plaćanje za poznati klub (pgz_admin / super_admin)"> Ručni unos</button>
<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);
if($('#fi-davatelj')) $('#fi-davatelj').addEventListener('change', refreshFinancije);
if($('#fi-sport')) $('#fi-sport').addEventListener('change', refreshFinancije);
if($('#fi-vrsta')) $('#fi-vrsta').addEventListener('change', refreshFinancije);
$('#fi-q').addEventListener('input', debounce(refreshFinancije, 200));
if($('#fi-samo-klubovi')) $('#fi-samo-klubovi').addEventListener('change', refreshFinancije);
if($('#fi-manual-btn')) $('#fi-manual-btn').addEventListener('click', openManualFinancijeForm);
refreshFinancije();
}
async function refreshFinancije(){
const god = $('#fi-god').value;
const dav = $('#fi-davatelj') ? $('#fi-davatelj').value : 'all';
const sport = $('#fi-sport') ? $('#fi-sport').value : '';
const vrsta = $('#fi-vrsta') ? $('#fi-vrsta').value : '';
const q = ($('#fi-q').value || '').toLowerCase().trim();
// PGŽ + Grad Rijeka live in pgz_sport.sufinanciranje_sport (served by /v2/potpore/by-year).
// RSS (Riječki sportski savez) lives separately and is served by /dashboard/top-primatelji.
// Merge both into a unified row shape so the user can filter "Svi izvori" in one table.
let pgzRijekaParam = '';
if (dav === 'grad_rijeka') pgzRijekaParam = '&davatelj=rijeka';
else if (dav === 'pgz') pgzRijekaParam = '&davatelj=pgz';
// For dav='rss' we skip /v2/potpore/by-year entirely; for dav='all' we keep it unfiltered.
const fetchPgzRijeka = (dav !== 'rss');
const fetchRss = (dav === 'all' || dav === 'rss');
const samoKlubovi = $('#fi-samo-klubovi') ? !!$('#fi-samo-klubovi').checked : true;
const params = `?godina=${god}${pgzRijekaParam}${sport?'&sport='+encodeURIComponent(sport):''}${vrsta?'&vrsta='+encodeURIComponent(vrsta):''}&samo_klubovi=${samoKlubovi}`;
const [analytics, byyear, rssJson] = await Promise.all([
api('/v2/analytics/proracun-sport?godina='+god),
fetchPgzRijeka ? api('/v2/potpore/by-year'+params) : Promise.resolve({results:[], total:0}),
fetchRss ? api('/dashboard/top-primatelji?godina='+god+'&limit=500').catch(()=>({rows:[]})) : Promise.resolve({rows:[]}),
]);
// Normalize PGŽ + Grad Rijeka rows: izvor stays 'rijeka.hr' or 'www2.pgz.hr'.
const pgzRijekaRows = (byyear && byyear.results) || [];
// Normalize RSS rows from /dashboard/top-primatelji to the same shape used by the table.
const rssRows = ((rssJson && rssJson.rows) || []).map(r => ({
korisnik: r.naziv_kluba,
sport: r.sport,
vrsta: r.vrsta || 'javne_potrebe',
iznos_eur: r.iznos,
razina: 'rss',
izvor: 'rss.hr',
source_url: r.pdf_url, // /sport/api/v2/dokumenti/godisnjak/<god>
godina: r.godina,
klub_id: r.klub_id,
napomena: r.napomena_short || r.napomena,
_davatelj_label: r.davatelj || 'RSS',
}));
// Apply the in-flight sport / vrsta filter to RSS too (by-year already applies it server-side).
let rssFiltered = rssRows;
if (sport) rssFiltered = rssFiltered.filter(r => (r.sport||'').toLowerCase().includes(sport.toLowerCase()));
if (vrsta) rssFiltered = rssFiltered.filter(r => (r.vrsta||'').toLowerCase().includes(vrsta.toLowerCase()));
let rows = pgzRijekaRows.concat(rssFiltered);
if(q) rows = rows.filter(r => (r.korisnik||'').toLowerCase().includes(q) || (r.sport||'').toLowerCase().includes(q));
const total = rows.reduce((s,r) => s + Number(r.iznos_eur||0), 0);
const poSportu = (analytics && analytics.po_sportu) || [];
$('#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><th>#</th>${sortHeader('financije','korisnik','Korisnik','')}${sortHeader('financije','sport','Sport','')}${sortHeader('financije','godina','God','num')}${sortHeader('financije','iznos_eur','Iznos','num')}${sortHeader('financije','izvor','Izvor','')}<th>PDF</th></tr></thead>
<tbody>${sortedRows.map((r,i) => {
const rid = 'fi-row-' + i;
const izvLabel = financijeIzvorLabel(r.izvor);
const pdfBtn = r.source_url
? '<a href="'+esc(financijePdfUrl(r))+'" target="_blank" rel="noreferrer" onclick="event.stopPropagation()" class="btn sm" title="Otvori izvorni PDF i skoči na klub">📄 Otvori PDF</a>'
: '<span style="color:var(--t3)">—</span>';
return `
<tr id="${rid}" onclick='toggleFinancijeDrill(${JSON.stringify(rid).replace(/'/g,"&#39;")}, ${JSON.stringify(r.korisnik).replace(/'/g,"&#39;")})' style="cursor:pointer">
<td style="width:24px;text-align:center;color:var(--t3)">▸</td>
<td>${i+1}</td>
<td><b>${esc(r.korisnik)}</b></td>
<td>${txt(r.sport)}</td>
<td class="num">${esc(r.godina||'')}</td>
<td class="num"><b>${fmtEurFull(r.iznos_eur)}</b></td>
<td><span class="badge" style="font-size:10px">${esc(izvLabel)}</span></td>
<td onclick="event.stopPropagation()">${pdfBtn}</td>
</tr>`;
}).join('')}
</tbody>
</table></div>`;
}
}
// ───────────── helpers added 2026-05-09 (Task 03b) ─────────────
// Map raw izvor strings → human label.
function financijeIzvorLabel(izvor){
const v = (izvor||'').toLowerCase();
if (v.includes('rijeka.hr')) return 'Grad Rijeka';
if (v.includes('rss')) return 'RSS';
if (v.includes('pgz')) return 'PGŽ';
return izvor || '—';
}
// Build a PDF link with #search=<korisnik> so most browsers' built-in PDF viewer
// scrolls to the first match. Falls back gracefully when source_url is missing.
function financijePdfUrl(r){
if (!r.source_url) return '#';
const sep = r.source_url.includes('#') ? '&' : '#';
// PDF.js + Chrome built-in viewer respect "#search=…"; for non-PDF docs the
// fragment is harmless.
return r.source_url + sep + 'search=' + encodeURIComponent(r.korisnik || '');
}
// Toggle inline expansion under the clicked row. Loads all-years × all-izvor
// rows for the same korisnik and renders a sub-table grouped by izvor.
async function toggleFinancijeDrill(rowId, korisnik){
const row = document.getElementById(rowId);
if (!row) return;
const next = row.nextElementSibling;
if (next && next.classList && next.classList.contains('fi-drill')) {
next.remove();
row.firstElementChild.textContent = '▸';
return;
}
row.firstElementChild.textContent = '▾';
const drill = document.createElement('tr');
drill.className = 'fi-drill';
const colspan = row.children.length;
drill.innerHTML = `<td colspan="${colspan}" style="background:var(--bg2);padding:10px 16px"><div class="loading">Učitavanje povijesti za <b>${esc(korisnik)}</b>…</div></td>`;
row.parentNode.insertBefore(drill, row.nextSibling);
// Fetch all years from both data sources (by-year accepts q, top-primatelji loops years).
const meta = window._fiMeta || (window._fiMeta = await api('/v2/potpore/meta'));
const years = (meta.godine || []).map(g => g.godina);
// Cap to last ~12 years to avoid blowing the request count.
const yearList = years.slice(0, 12);
const aggregateByYear = await api('/v2/potpore/aggregate?q=' + encodeURIComponent(korisnik) + '&limit=50').catch(() => ({results:[]}));
// For RSS we have to loop years (no per-korisnik filter on top-primatelji).
const rssFetches = await Promise.all(yearList.map(y =>
api('/dashboard/top-primatelji?godina=' + y + '&limit=500').catch(() => ({rows:[]}))
));
const rssAll = [];
rssFetches.forEach(j => (j.rows || []).forEach(r => {
if ((r.naziv_kluba||'').toLowerCase().includes(korisnik.toLowerCase())) {
rssAll.push({
korisnik: r.naziv_kluba, sport: r.sport, godina: r.godina,
iznos_eur: r.iznos, izvor: 'rss.hr', source_url: r.pdf_url,
klub_id: r.klub_id,
});
}
}));
// Pull aggregate hits as flattened rows (one per matched korisnik · tip group).
const aggRows = (aggregateByYear.results || []).filter(a =>
(a.korisnik||'').toLowerCase() === korisnik.toLowerCase()
);
// Build per-izvor summary.
const all = rssAll.slice();
// Add aggregate results as summary rows (they sum iznos across years per tip).
const aggSummary = aggRows.map(a => ({
korisnik: a.korisnik, sport: a.sport, godina: `${a.od_god}${a.do_god}`,
iznos_eur: a.ukupno_eur, izvor: a.izvori || a.tip,
source_url: a.source_url, _is_summary: true, _n: a.n_potpore,
}));
if (all.length === 0 && aggSummary.length === 0) {
drill.firstChild.innerHTML = `<div class="empty" style="padding:8px">Nema dodatnih zapisa za <b>${esc(korisnik)}</b>.</div>`;
return;
}
const groupHtml = (label, rows) => {
if (!rows.length) return '';
const sum = rows.reduce((s,r) => s + Number(r.iznos_eur||0), 0);
return `
<div style="margin-top:8px">
<div style="font-size:11px;color:var(--t2);text-transform:uppercase;letter-spacing:0.6px;margin-bottom:4px">${label} <span style="color:var(--t3)">· ${rows.length} stavki · ${fmtEurFull(sum)}</span></div>
<table style="width:100%;font-size:12px">
<thead><tr><th style="text-align:left">Godina</th><th style="text-align:left">Sport / razina</th><th class="num">Iznos</th><th>Izvor</th><th>PDF</th></tr></thead>
<tbody>${rows.map(r => {
const pdf = r.source_url ? '<a href="'+esc(financijePdfUrl(r))+'" target="_blank" onclick="event.stopPropagation()">📄</a>' : '—';
return `<tr><td>${esc(r.godina)}</td><td>${txt(r.sport)}</td><td class="num"><b>${fmtEurFull(r.iznos_eur)}</b></td><td>${esc(financijeIzvorLabel(r.izvor))}</td><td>${pdf}</td></tr>`;
}).join('')}</tbody>
</table>
</div>`;
};
drill.firstChild.innerHTML = `
<div style="padding:4px 0">
<div style="font-weight:700;font-size:13px;margin-bottom:4px">📜 Povijest financiranja: ${esc(korisnik)}</div>
${groupHtml('🌊 RSS — godišnjaci', rssAll.sort((a,b)=>b.godina-a.godina))}
${groupHtml('🏛 PGŽ + 🌆 Grad Rijeka — agregat', aggSummary)}
</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','sportovi','Sport','')}${sortHeader('objekti','kapacitet','Kapacitet','num')}<th>Adresa</th>${sortHeader('objekti','upravitelj','Upravitelj / klub','')}<th>Karta</th></tr></thead>
<tbody>${rows.map(o => {
const sportLabel = Array.isArray(o.sportovi) && o.sportovi.length ? o.sportovi.slice(0,2).join(', ') + (o.sportovi.length>2?` +${o.sportovi.length-2}`:'') : '—';
const adresaLabel = [o.adresa, o.grad].filter(Boolean).join(', ') || '—';
return `
<tr onclick="openObjekt(${o.id})">
<td><b>${esc(o.naziv)}</b></td>
<td>${txt(o.tip)}</td>
<td>${esc(sportLabel)}</td>
<td class="num">${o.kapacitet ? fmtNum(o.kapacitet) : '—'}</td>
<td>${esc(adresaLabel)}</td>
<td>${txt(o.upravitelj)}</td>
<td onclick="event.stopPropagation()">${objektMapsButton(o)}</td>
</tr>`;
}).join('')}</tbody>
</table></div>`;
}
// Build a Google Maps deep-link button for an objekt row.
// Spec: prefer lat/lng if present, fall back to address search. We
// additionally offer the address-search button alongside the pin so
// users can sanity-check pin placement (some seed coordinates were
// inaccurate — see 22_objekti_maps.md).
function objektMapsButton(o){
const addrParts = [o.naziv, o.adresa, o.grad].filter(Boolean).join(', ') || (o.naziv || '');
const addrUrl = 'https://www.google.com/maps/search/?api=1&query=' + encodeURIComponent(addrParts);
if (o.lat != null && o.lng != null) {
const pinUrl = 'https://www.google.com/maps/search/?api=1&query=' + Number(o.lat) + ',' + Number(o.lng);
return (
'<a class="btn sm" href="' + esc(pinUrl) + '" target="_blank" rel="noreferrer" title="Otvori spremljene koordinate u Google Maps">📍 Pin</a> ' +
'<a class="btn sm" href="' + esc(addrUrl) + '" target="_blank" rel="noreferrer" title="Otvori adresu (sigurnija opcija ako pin izgleda krivo)">🔍 Adresa</a>'
);
}
return '<a class="btn sm" href="' + esc(addrUrl) + '" target="_blank" rel="noreferrer" title="Otvori adresu u Google Maps (nema spremljenih koordinata)">🔍 Adresa</a>';
}
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">${o.upravitelj_oib?formatOib(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 ===========
// View mode persisted in localStorage as `_manifViewMode` ('card'|'table')
const _manifFilter = {mjesto:'', razina:'', organizator:'', q:'', godina:'', sport:'', savez_id:''};
let _manifMeta = null;
let _manifLoadSeq = 0;
const _manifExpanded = new Set();
async function loadManifestacije(){
const root = $('#pg-manifestacije');
// Restore view mode from localStorage
const saved = localStorage.getItem('_manifViewMode');
if(saved==='card' || saved==='table') _state.viewManif = saved;
if(!_manifMeta){
root.innerHTML = '<div class="loading">Učitavanje manifestacija…</div>';
_manifMeta = await api('/v2/manifestacije/meta') || {mjesta:[], razine:[], organizatori:[], godine:[], sportovi:[], savezi:[]};
}
renderManifShell();
await reloadManifestacije();
}
async function reloadManifestacije(){
const seq = ++_manifLoadSeq;
const out = $('#mn-out');
if(out) out.innerHTML = '<div class="loading">Učitavanje…</div>';
const cnt = $('#mn-cnt');
if(cnt) cnt.textContent = '…';
const params = new URLSearchParams();
if(_manifFilter.mjesto) params.set('mjesto', _manifFilter.mjesto);
if(_manifFilter.razina) params.set('razina', _manifFilter.razina);
if(_manifFilter.organizator) params.set('organizator', _manifFilter.organizator);
if(_manifFilter.q) params.set('q', _manifFilter.q);
if(_manifFilter.godina) params.set('godina', _manifFilter.godina);
if(_manifFilter.sport) params.set('sport', _manifFilter.sport);
if(_manifFilter.savez_id) params.set('savez_id', _manifFilter.savez_id);
params.set('limit', '500');
const qs = params.toString();
const d = await api('/v2/manifestacije'+(qs?'?'+qs:''));
if(seq !== _manifLoadSeq) return; // newer request superseded this one
if(!d){
if(out) out.innerHTML = '<div class="empty">Greška pri dohvatu</div>';
return;
}
_cache.manifestacije = d.rows || [];
_manifExpanded.clear();
renderManifBody();
}
function renderManifShell(){
const root = $('#pg-manifestacije');
const meta = _manifMeta || {mjesta:[], razine:[], organizatori:[], godine:[], sportovi:[], savezi:[]};
const optList = (arr) => (arr||[]).filter(x=>x!==null && x!==undefined && x!=='').map(v=>'<option value="'+esc(v)+'">'+esc(v)+'</option>').join('');
const optSavezi = (meta.savezi||[]).map(s=>'<option value="'+esc(s.id)+'">'+esc(s.naziv)+'</option>').join('');
root.innerHTML = `
<div class="toolbar">
<input type="search" id="mn-q" placeholder="🔍 Pretraži manifestaciju…" value="${esc(_manifFilter.q)}">
<select id="mn-godina" title="Godina"><option value="">Sve godine</option>${optList(meta.godine)}</select>
<select id="mn-sport" title="Sport"><option value="">Svi sportovi</option>${optList(meta.sportovi)}</select>
<select id="mn-savez" title="Savez"><option value="">Svi savezi</option>${optSavezi}</select>
<select id="mn-mjesto" title="Mjesto"><option value="">Sva mjesta</option>${optList(meta.mjesta)}</select>
<select id="mn-raz" title="Razina"><option value="">Sve razine</option>${optList(meta.razine)}</select>
<select id="mn-org" title="Organizator"><option value="">Svi organizatori</option>${optList(meta.organizatori)}</select>
<button id="mn-reset" class="btn" type="button" title="Poništi filtere">↺ Reset</button>
<div class="toggle">
<button id="mn-card" class="${_state.viewManif==='card'?'active':''}" onclick="setManifView('card')" title="Kartice">🃏 Kartice</button>
<button id="mn-table" class="${_state.viewManif==='table'?'active':''}" onclick="setManifView('table')" title="Tablica">📋 Tablica</button>
</div>
<span class="tb-s" id="mn-cnt"></span>
</div>
<div id="mn-out"></div>
`;
// Restore selections after re-render
if($('#mn-mjesto')) $('#mn-mjesto').value = _manifFilter.mjesto;
if($('#mn-raz')) $('#mn-raz').value = _manifFilter.razina;
if($('#mn-org')) $('#mn-org').value = _manifFilter.organizator;
if($('#mn-godina')) $('#mn-godina').value = _manifFilter.godina;
if($('#mn-sport')) $('#mn-sport').value = _manifFilter.sport;
if($('#mn-savez')) $('#mn-savez').value = _manifFilter.savez_id;
$('#mn-q').addEventListener('input', debounce(()=>{ _manifFilter.q = $('#mn-q').value.trim(); reloadManifestacije(); }, 250));
$('#mn-mjesto').addEventListener('change', ()=>{ _manifFilter.mjesto = $('#mn-mjesto').value; reloadManifestacije(); });
$('#mn-raz').addEventListener('change', ()=>{ _manifFilter.razina = $('#mn-raz').value; reloadManifestacije(); });
$('#mn-org').addEventListener('change', ()=>{ _manifFilter.organizator = $('#mn-org').value; reloadManifestacije(); });
$('#mn-godina').addEventListener('change', ()=>{ _manifFilter.godina = $('#mn-godina').value; reloadManifestacije(); });
$('#mn-sport').addEventListener('change', ()=>{ _manifFilter.sport = $('#mn-sport').value; reloadManifestacije(); });
$('#mn-savez').addEventListener('change', ()=>{ _manifFilter.savez_id = $('#mn-savez').value; reloadManifestacije(); });
$('#mn-reset').addEventListener('click', ()=>{
_manifFilter.mjesto=''; _manifFilter.razina=''; _manifFilter.organizator=''; _manifFilter.q='';
_manifFilter.godina=''; _manifFilter.sport=''; _manifFilter.savez_id='';
$('#mn-q').value=''; $('#mn-mjesto').value=''; $('#mn-raz').value=''; $('#mn-org').value='';
$('#mn-godina').value=''; $('#mn-sport').value=''; $('#mn-savez').value='';
reloadManifestacije();
});
}
function setManifView(v){
_state.viewManif = v;
try{ localStorage.setItem('_manifViewMode', v); }catch(_){}
if($('#mn-card')) $('#mn-card').classList.toggle('active', v==='card');
if($('#mn-table')) $('#mn-table').classList.toggle('active', v==='table');
renderManifBody();
}
function renderManifBody(){
let rows = _cache.manifestacije || [];
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);
}
// Backwards-compat: existing handlers (e.g. sortHeader) call applyManifFilter()
function applyManifFilter(){ renderManifBody(); }
function manifDetailHTML(m){
const gq = encodeURIComponent((m.naziv||'')+' '+(m.mjesto||'')+' sport');
const googleUrl = 'https://www.google.com/search?q='+gq;
const created = m.created_at ? new Date(m.created_at).toLocaleDateString('hr') : '—';
const scraped = m.last_scraped_at ? new Date(m.last_scraped_at).toLocaleDateString('hr') : '—';
const savezLink = m.savez_id ? '<a href="#" onclick="event.preventDefault();event.stopPropagation();openSavez('+m.savez_id+')">'+esc(m.savez_naziv||('Savez '+m.savez_id))+'</a>' : '—';
return `
<div class="kv">
<div class="k">Sport</div><div class="v">${m.sport?'<span class="tag b">'+esc(m.sport)+'</span>':'—'}</div>
<div class="k">Savez</div><div class="v">${savezLink}</div>
<div class="k">Razina</div><div class="v">${m.razina?'<span class="tag b">'+esc(m.razina)+'</span>':'—'}</div>
<div class="k">Mjesto</div><div class="v">${txt(m.mjesto)}</div>
<div class="k">Organizator (klub)</div><div class="v">${txt(m.organizator)}</div>
<div class="k">Godina od</div><div class="v">${txt(m.godina_od)}</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">Izvor</div><div class="v">${txt(m.source)}</div>
<div class="k">Web savez</div><div class="v">${m.savez_web?'<a href="'+esc(m.savez_web)+'" target="_blank" rel="noopener" onclick="event.stopPropagation()">'+esc(m.savez_web)+' ↗</a>':'—'}</div>
<div class="k">Unos</div><div class="v">${esc(created)}</div>
<div class="k">Zadnji scrape</div><div class="v">${esc(scraped)}</div>
</div>
${m.napomena ? '<div style="margin-top:10px;font-size:12px;line-height:1.5;color:var(--t1);padding:10px;background:var(--bg3);border-radius:5px">'+esc(m.napomena)+'</div>' : ''}
<div style="margin-top:10px;text-align:right">
<a href="${googleUrl}" target="_blank" class="btn" rel="noopener" onclick="event.stopPropagation()" style="text-decoration:none">🔍 Google</a>
</div>
`;
}
function manifOtvoriBtn(m){
if(m.source_url){
return '<a class="btn primary" href="'+esc(m.source_url)+'" target="_blank" rel="noopener" onclick="event.stopPropagation()" style="text-decoration:none">Otvori ↗</a>';
}
const open = _manifExpanded.has(m.id);
return '<button class="btn" type="button" onclick="event.stopPropagation();toggleManif('+m.id+')">'+(open?'▴ Sakrij':'▾ Detalji')+'</button>';
}
function renderManifGrid(rows){
if(!rows.length) return '<div class="empty">Nema manifestacija za zadane filtere</div>';
return '<div class="grid">'+rows.map(m => {
const open = _manifExpanded.has(m.id);
const sportTag = m.sport ? '<span class="tag b">'+esc(m.sport)+'</span>' : '';
return `
<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">${sportTag}${m.godina_od?' <span class="tag">'+esc(m.godina_od)+'</span>':''} ${txt(m.mjesto,'—')}</div>
<div class="em">
${m.organizator?'<span>'+esc((m.organizator||'').slice(0,50))+'</span>':''}
${m.broj_ucesnika?'<span><b>'+esc(m.broj_ucesnika)+'</b> sudionika</span>':''}
</div>
<div style="margin-top:8px;text-align:right">${manifOtvoriBtn(m)}</div>
${open?'<div class="card" style="margin-top:10px;padding:10px" onclick="event.stopPropagation()">'+manifDetailHTML(m)+'</div>':''}
</div>`;
}).join('')+'</div>';
}
function renderManifTable(rows){
if(!rows.length) return '<div class="empty">Nema manifestacija za zadane filtere</div>';
return `<div class="card" style="padding:0;overflow-x:auto"><table>
<thead><tr>${sortHeader('manifestacije','naziv','Naziv','')}${sortHeader('manifestacije','sport','Sport','')}${sortHeader('manifestacije','godina_od','Datum','')}${sortHeader('manifestacije','mjesto','Mjesto','')}${sortHeader('manifestacije','organizator','Klub/Organizator','')}<th></th></tr></thead>
<tbody>${rows.map(m => {
const open = _manifExpanded.has(m.id);
return `
<tr onclick="openManif(${m.id})" style="cursor:pointer">
<td><b>${esc(m.naziv)}</b>${m.razina?' <span class="tag b">'+esc(m.razina)+'</span>':''}</td>
<td>${m.sport?'<span class="tag b">'+esc(m.sport)+'</span>':'—'}</td>
<td>${txt(m.godina_od)}</td>
<td>${txt(m.mjesto)}</td>
<td>${txt(m.organizator)}</td>
<td style="text-align:right">${manifOtvoriBtn(m)}</td>
</tr>
${open?'<tr onclick="event.stopPropagation()"><td colspan="6" style="background:var(--bg3);padding:14px">'+manifDetailHTML(m)+'</td></tr>':''}`;
}).join('')}</tbody>
</table></div>`;
}
function toggleManif(id){
if(_manifExpanded.has(id)) _manifExpanded.delete(id);
else _manifExpanded.add(id);
renderManifBody();
}
function openManif(id){
const m = (_cache.manifestacije||[]).find(x => x.id===id);
if(!m) return;
if(m.source_url){
window.open(m.source_url, '_blank', 'noopener');
return;
}
toggleManif(id);
}
//=========== MREŽA (Network Graph) ===========
const _mreza = {data:null, sim:null, allNodes:null, allEdges:null, filter:{osoba:'', klub:'', tvrtka:'', tip:''}, toggle:{osoba:true, entitet:true, klub_savez:true}};
// Heuristic to decide whether an entity is actually a sports klub/savez.
// Matches: NK/HNK/MOK/RK/KK/VK/HK/TK/JK/ŠK token, or the words "klub"/"savez" anywhere.
function _mrezaIsKlubSavez(label){
if(!label) return false;
const s = String(label);
if(/\b(NK|HNK|MOK|RK|KK|VK|HK|TK|JK|ŠK|FK|BK|GK)\b/.test(s)) return true;
if(/\b(klub|savez)\b/i.test(s)) return true;
return false;
}
// Map a node to one of three logical filter groups; returns null for nodes that
// shouldn't participate in toggling (e.g. user-injected suggestions).
function _mrezaCategory(n){
if(!n) return null;
if(n.type === 'person') return 'osoba';
if(n.type === 'pgz_savez') return 'klub_savez';
if(n.type === 'entity' || n.type === 'supplier'){
return _mrezaIsKlubSavez(n.label) ? 'klub_savez' : 'entitet';
}
return null; // injected / unknown — never hidden by toggles
}
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; }
const nodes = (resp.data.nodes||[]).slice();
const edges = (resp.data.edges||[]).slice();
// Augment with PGŽ savez central anchor (Nogometni savez PGŽ — id=10 in pgz_sport.savezi)
const anchorId = 'pgz-savez-nogometni';
if(!nodes.find(n => n.id === anchorId)){
nodes.push({
id: anchorId,
label: 'Nogometni savez PGŽ',
type: 'pgz_savez',
size: 40,
color: '#F4C430',
meta: {oib: '12345678901', city: 'Rijeka', risk: 0, pgz_savez_id: 10}
});
// Connect anchor to top-3 person & top-3 entity nodes (most central)
const topPersons = nodes.filter(n => n.type==='person').sort((a,b)=>(b.size||0)-(a.size||0)).slice(0,3);
const topEntities = nodes.filter(n => n.type==='entity').sort((a,b)=>(b.size||0)-(a.size||0)).slice(0,3);
for(const t of [...topPersons, ...topEntities]){
edges.push({source:anchorId, target:t.id, color:'#F4C43055', size:0.6});
}
}
// Tag each node with a logical filter category (osoba / entitet / klub_savez).
// Recolor sport klubovi/savezi so they visually align with the green Klub/Savez toggle,
// and bump procurement suppliers (which were also green) to amber to avoid the colour clash.
for(const n of nodes){
n.category = _mrezaCategory(n);
if(n.category === 'klub_savez' && n.type !== 'pgz_savez'){
n.color = '#00e68a';
} else if(n.type === 'supplier'){
n.color = '#ffaa00';
}
}
_mreza.data = {nodes, edges};
_mreza.allNodes = nodes;
_mreza.allEdges = edges;
_mreza.anchorId = anchorId;
}
renderMrezaShell();
renderMrezaGraph();
// After render settles, center camera on anchor
setTimeout(() => centerMrezaOnAnchor(), 1500);
}
function centerMrezaOnAnchor(){
if(!_mreza.graph) return;
const anchor = (_mreza.allNodes||[]).find(n => n.id === _mreza.anchorId);
if(!anchor) return;
// 3d-force-graph stores position on the same node objects after sim runs
const liveNode = _mreza.graph.graphData().nodes.find(n => n.id === anchor.id);
if(!liveNode) return;
const dist = 200;
const distRatio = 1 + dist/Math.max(1, Math.hypot(liveNode.x||1, liveNode.y||1, liveNode.z||1));
try{
_mreza.graph.cameraPosition(
{ x:(liveNode.x||0)*distRatio, y:(liveNode.y||0)*distRatio, z:(liveNode.z||0)*distRatio },
liveNode,
1500
);
}catch(e){ console.warn('center anchor', e); }
}
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.category==='osoba').length}</div></div>
<div class="kpi r"><div class="kpi-l">Klubova / saveza</div><div class="kpi-v">${(_mreza.allNodes||[]).filter(n=>n.category==='klub_savez').length}</div></div>
</div>
<div class="toolbar" style="margin-bottom:10px;align-items:flex-start">
<div class="ac-wrap" style="position:relative"><input type="search" id="mr-osoba" data-ac-type="person" placeholder="👤 Osoba… (Enter za prvi rezultat)"><div class="ac-drop" id="mr-osoba-drop"></div></div>
<div class="ac-wrap" style="position:relative"><input type="search" id="mr-klub" data-ac-type="club" placeholder="🏟 Klub / Savez…"><div class="ac-drop" id="mr-klub-drop"></div></div>
<div class="ac-wrap" style="position:relative"><input type="search" id="mr-tvrtka" data-ac-type="company" placeholder="🏢 Tvrtka / Entitet…"><div class="ac-drop" id="mr-tvrtka-drop"></div></div>
<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>
<button class="btn" onclick="centerMrezaOnAnchor()">🎯 Centar (PGŽ)</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 • Enter pretraga
</div>
</div>
<div class="card" style="margin-top:10px">
<div class="card-h"><div class="card-t">🎨 Legenda &amp; filteri tipova</div></div>
<div id="mr-toggles" style="display:flex;gap:8px;flex-wrap:wrap;font-size:12px;align-items:center">
<button type="button" class="mr-tg" data-cat="osoba" aria-pressed="true"
style="display:inline-flex;align-items:center;gap:6px;padding:7px 12px;min-height:36px;border-radius:18px;border:1px solid #283560;background:rgba(139,92,246,0.12);color:var(--t1);font-size:12px;cursor:pointer;line-height:1">
<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#8b5cf6"></span>Osoba <span class="mr-tg-n" style="opacity:.7">${(_mreza.allNodes||[]).filter(n=>n.category==='osoba').length}</span>
</button>
<button type="button" class="mr-tg" data-cat="entitet" aria-pressed="true"
style="display:inline-flex;align-items:center;gap:6px;padding:7px 12px;min-height:36px;border-radius:18px;border:1px solid #283560;background:rgba(255,68,102,0.12);color:var(--t1);font-size:12px;cursor:pointer;line-height:1">
<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#ff4466"></span>Entitet <span class="mr-tg-n" style="opacity:.7">${(_mreza.allNodes||[]).filter(n=>n.category==='entitet').length}</span>
</button>
<button type="button" class="mr-tg" data-cat="klub_savez" aria-pressed="true"
style="display:inline-flex;align-items:center;gap:6px;padding:7px 12px;min-height:36px;border-radius:18px;border:1px solid #283560;background:rgba(0,230,138,0.14);color:var(--t1);font-size:12px;cursor:pointer;line-height:1">
<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#00e68a"></span>Klub/Savez <span class="mr-tg-n" style="opacity:.7">${(_mreza.allNodes||[]).filter(n=>n.category==='klub_savez').length}</span>
</button>
<div style="color:var(--t2);margin-left:6px">Klikni za uključi/isključi · Veličina = risk / promet · 3D force graph (drag rotate, scroll zoom)</div>
</div>
</div>
`;
// Wire autocomplete + filter on the 3 search inputs
['#mr-osoba', '#mr-klub', '#mr-tvrtka'].forEach(sel => {
const el = $(sel);
if(!el) return;
el.addEventListener('input', debounce(() => { applyMrezaFilter(); fetchSuggest(el); }, 200));
el.addEventListener('keydown', e => {
if(e.key === 'Enter'){ e.preventDefault(); pickFirstSuggest(el); }
if(e.key === 'Escape'){ closeSuggest(el); }
});
el.addEventListener('blur', () => setTimeout(() => closeSuggest(el), 200));
});
$('#mr-tip').addEventListener('change', applyMrezaFilter);
// Wire 3 category toggle pills (Osoba / Entitet / Klub-Savez)
document.querySelectorAll('#mr-toggles .mr-tg').forEach(btn => {
// Sync visual state with current _mreza.toggle (preserve across re-render)
const cat = btn.dataset.cat;
if(_mreza.toggle && _mreza.toggle[cat] === false){
btn.setAttribute('aria-pressed','false');
btn.style.opacity = '0.4';
btn.style.textDecoration = 'line-through';
}
btn.addEventListener('click', () => {
const c = btn.dataset.cat;
_mreza.toggle[c] = !_mreza.toggle[c];
const on = _mreza.toggle[c];
btn.setAttribute('aria-pressed', on ? 'true' : 'false');
btn.style.opacity = on ? '1' : '0.4';
btn.style.textDecoration = on ? 'none' : 'line-through';
applyMrezaFilter();
});
});
}
async function fetchSuggest(inputEl){
const q = (inputEl.value||'').trim();
const drop = document.getElementById(inputEl.id + '-drop');
if(!drop) return;
if(q.length < 2){ drop.innerHTML = ''; drop.style.display='none'; return; }
const type = inputEl.dataset.acType || '';
const r = await api('/v2/search/suggest?q='+encodeURIComponent(q)+'&type='+type+'&limit=10');
if(!r){ drop.innerHTML=''; drop.style.display='none'; return; }
const results = r.results || [];
if(!results.length){ drop.innerHTML='<div class="ac-empty">Nema rezultata</div>'; drop.style.display='block'; return; }
drop.innerHTML = results.map(s => `
<div class="ac-item" data-id="${esc(s.id)}" data-label="${esc(s.label)}" onclick="pickSuggest('${inputEl.id}',this)">
<div class="ac-l">${esc(s.label)}</div>
<div class="ac-s">${esc(s.sub||s.type||'')}</div>
</div>
`).join('');
drop.style.display = 'block';
drop.dataset.firstId = results[0].id;
drop.dataset.firstLabel = results[0].label;
}
function pickSuggest(inputElId, itemEl){
const inputEl = document.getElementById(inputElId);
const id = itemEl.dataset.id;
const label = itemEl.dataset.label;
if(inputEl){ inputEl.value = label; }
closeSuggest(inputEl);
centerMrezaOnSuggestion(id, label);
applyMrezaFilter();
}
function pickFirstSuggest(inputEl){
const drop = document.getElementById(inputEl.id + '-drop');
if(drop && drop.dataset.firstId){
inputEl.value = drop.dataset.firstLabel || '';
centerMrezaOnSuggestion(drop.dataset.firstId, drop.dataset.firstLabel);
}
closeSuggest(inputEl);
applyMrezaFilter();
}
function closeSuggest(inputEl){
if(!inputEl) return;
const drop = document.getElementById(inputEl.id + '-drop');
if(drop){ drop.style.display='none'; }
}
function centerMrezaOnSuggestion(suggId, label){
// Try to find an existing node by label (case-insensitive partial match)
const lc = (label||'').toLowerCase();
const live = _mreza.graph ? _mreza.graph.graphData().nodes : (_mreza.allNodes||[]);
let target = live.find(n => (n.label||'').toLowerCase() === lc);
if(!target) target = live.find(n => (n.label||'').toLowerCase().includes(lc));
if(!target && _mreza.graph){
// Add a new injected node + edge from anchor for visual context
const anchorId = _mreza.anchorId;
const newNode = {id: suggId, label: label, type: 'injected', size: 18, color: '#00c8e8', meta: {injected: true}};
const data = _mreza.graph.graphData();
data.nodes.push(newNode);
if(anchorId) data.links.push({source: anchorId, target: suggId, color:'#00c8e855', size:0.8});
_mreza.graph.graphData(data);
_mreza.allNodes.push(newNode);
if(anchorId) _mreza.allEdges.push({source: anchorId, target: suggId, color:'#00c8e855', size:0.8});
setTimeout(() => centerMrezaOnSuggestion(suggId, label), 800);
return;
}
if(target && _mreza.graph){
const dist = 120;
const x = target.x||0, y = target.y||0, z = target.z||0;
const r = 1 + dist/Math.max(1, Math.hypot(x||1,y||1,z||1));
try{ _mreza.graph.cameraPosition({x:x*r, y:y*r, z:z*r}, target, 1200); }catch(e){}
}
}
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();
// Category toggles: hide nodes whose category is switched off (null category = always shown).
const tg = _mreza.toggle || {osoba:true, entitet:true, klub_savez:true};
nodes = nodes.filter(n => {
const c = n.category;
if(!c) return true;
return tg[c] !== false;
});
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(formatOib(m.oib,{klub_id:m.klub_id,savez_id:m.savez_id}))+'</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>
${customFindingEnrichBlock(c.id, c.osoba || c.naslov)}
`;
openPanel('Forenzika · '+c.naslov, html);
}
function customFindingEnrichBlock(customId, queryName){
const safeId = String(customId).replace(/[^a-z0-9_-]/gi,'_');
return `
<div class="card" id="fenrich-card-${safeId}">
<div class="card-h">
<div class="card-t">✨ Obogati podatke (Wikipedia)</div>
<button class="btn primary" onclick="enrichCustomFinding('${safeId}', '${esc(queryName).replace(/'/g,'\\\\&#39;')}')">▶ Pokreni</button>
</div>
<div id="fenrich-out-${safeId}">
<div class="empty" style="padding:14px">Lookup Wikipedia HR za "${esc(queryName)}" i prikaži dopune.</div>
</div>
</div>
`;
}
async function enrichCustomFinding(safeId, queryName){
const out = document.getElementById('fenrich-out-'+safeId);
if(out) out.innerHTML = '<div class="loading">Lookup Wikipedia HR…</div>';
// Custom findings aren't in DB — call wiki lookup via a synthesised forensic_findings.id of -1 won't work.
// Instead, use the existing /v2/enrich/sportas pattern to query Wikipedia by name.
// We re-use the wiki summary via a mini fetch helper.
try{
const wiki = await fetch('https://hr.wikipedia.org/api/rest_v1/page/summary/'+encodeURIComponent(queryName.replace(/ /g,'_')))
.then(r => r.ok ? r.json() : null).catch(()=>null);
if(!wiki || wiki.type==='disambiguation' || !wiki.extract){
if(out) out.innerHTML = '<div class="empty" style="padding:14px">Nije pronađen Wikipedia HR članak za <b>'+esc(queryName)+'</b>.</div>';
return;
}
const w = {title: wiki.title, extract: wiki.extract, description: wiki.description, url: (wiki.content_urls||{}).desktop?.page};
if(out) out.innerHTML = `
<div style="margin-bottom:8px"><span class="tag gr">🟢 Wikipedia HR</span></div>
<div style="padding:10px;background:var(--bg3);border-left:3px solid var(--pgz-gold);border-radius:5px">
<div style="font-weight:700;color:var(--t0);font-size:14px;margin-bottom:6px">${esc(w.title||'')}</div>
${w.description?'<div style="font-size:11px;color:var(--t2);margin-bottom:6px;font-style:italic">'+esc(w.description)+'</div>':''}
${w.extract?'<div style="font-size:12px;line-height:1.6;color:var(--t1)">'+esc(w.extract)+'</div>':''}
${w.url?'<div style="margin-top:8px"><a href="'+esc(w.url)+'" target="_blank">↗ Otvori članak</a></div>':''}
</div>`;
}catch(e){
if(out) out.innerHTML = '<div class="empty" style="color:var(--red);padding:14px">Greška: '+esc(String(e))+'</div>';
}
}
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="panelDrill(openKlub,'+a.klub_id+')"><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="panelDrill(openSportas,'+a.clan_id+')"><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>
${forensicEnrichBlock(a.id)}
`;
openPanel('Alarm #'+a.id, html);
}
function forensicEnrichBlock(findingId){
return `
<div class="card" id="fenrich-card-${findingId}">
<div class="card-h">
<div class="card-t">✨ Obogati podatke (Wikipedia)</div>
<button class="btn primary" onclick="enrichForensicFinding(${findingId})">▶ Pokreni</button>
</div>
<div id="fenrich-out-${findingId}">
<div class="empty" style="padding:14px">Ekstrakcija imena iz nalaza, lookup na Wikipedia HR i sprema u DB. Drugi puta će biti vidljivo bez ponovnog skidanja.</div>
</div>
</div>
`;
}
async function enrichForensicFinding(findingId){
const out = document.getElementById('fenrich-out-'+findingId);
if(out) out.innerHTML = '<div class="loading">Ekstraktiram ime, lookup Wikipedia HR…</div>';
const r = await apiPost('/v2/forensic/findings/'+findingId+'/enrich');
if(!r){ if(out) out.innerHTML = '<div class="empty" style="color:var(--red)">Greška</div>'; return; }
const w = r.wiki || null;
const html = `
<div style="margin-bottom:8px">
<span class="tag gr">🟢 Persisted</span>
${r.used_query?'<span class="tag b">query: '+esc(r.used_query)+'</span>':''}
</div>
${w ? `
<div style="padding:10px;background:var(--bg3);border-left:3px solid var(--pgz-gold);border-radius:5px">
<div style="font-size:11px;color:var(--t4);text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px">📚 Wikipedia HR</div>
<div style="font-weight:700;color:var(--t0);font-size:14px;margin-bottom:6px">${esc(w.title||'')}</div>
${w.description?'<div style="font-size:11px;color:var(--t2);margin-bottom:6px;font-style:italic">'+esc(w.description)+'</div>':''}
${w.extract?'<div style="font-size:12px;line-height:1.6;color:var(--t1)">'+esc(w.extract)+'</div>':''}
${w.url?'<div style="margin-top:8px"><a href="'+esc(w.url)+'" target="_blank">↗ Otvori članak</a></div>':''}
</div>
` : '<div class="empty" style="padding:14px">Nije pronađen Wikipedia HR članak za ekstrahirana imena.<br>Pokušaji: '+esc(JSON.stringify(r.queried||[]))+'</div>'}
`;
if(out) out.innerHTML = 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?(canSeeFullOib({klub_id:p.klub_id,savez_id:p.savez_id})?'<a class="tag" onclick="openOIB(&quot;'+esc(p.oib)+'&quot;)" style="cursor:pointer">OIB '+esc(p.oib)+'</a>':'<span class="tag">OIB '+esc(maskOib(p.oib))+'</span>'):''}</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);
// PRIORITY FILTERS (CRISIS V5) — default: prikazujemo samo klubove koji imaju podatke
window._klub_only_priority = true; // by default: only klubovi koji primaju novac ili su u godisnjaku ili imaju HNS roster
window._sportas_only_priority = true; // by default: only iz priority klubova
window._sportas_only_hns = false; // dodatni filter: samo s HNS profilom
window.toggleKlubPriority = function(){
window._klub_only_priority = !window._klub_only_priority;
if(typeof loadKlubovi === 'function') loadKlubovi();
};
window.toggleSportasPriority = function(){
window._sportas_only_priority = !window._sportas_only_priority;
if(typeof loadSportasi === 'function') loadSportasi();
};
window.toggleSportasHNS = function(){
window._sportas_only_hns = !window._sportas_only_hns;
if(typeof loadSportasi === 'function') loadSportasi();
};
// PANEL HISTORY STACK + NATRAG (CRISIS V7 / BUG-B opener-based)
// Each entry: {opener: <fn name>, args: [...]} — panelBack re-runs the previous opener.
window._panelHistory = [];
window._panelDrilling = false; // suppress root-clear while drilling
window._panelSuppressPush = false; // suppress re-push when re-running for back nav
window._panelOpenerMap = {}; // name -> fn
window._registerOpener = function(fn){
if(typeof fn !== 'function' || !fn.name) return null;
window._panelOpenerMap[fn.name] = fn;
return fn.name;
};
window._updateBackBtn = function(){
const back = document.getElementById('panel-back');
if(back) back.style.display = (window._panelHistory.length > 1) ? 'inline-flex' : 'none';
};
// Push opener+args; record in browser history too (for browser back button)
window.pushPanelState = function(opener, args){
if(window._panelSuppressPush) return;
const name = (typeof opener === 'function') ? window._registerOpener(opener) : (typeof opener === 'string' ? opener : null);
if(!name) return;
const top = window._panelHistory[window._panelHistory.length - 1];
const sig = name + ':' + JSON.stringify(args || []);
if(top && (top.opener + ':' + JSON.stringify(top.args || [])) === sig) return;
window._panelHistory.push({opener: name, args: args || []});
try { history.pushState({pgzPanel: true, depth: window._panelHistory.length}, '', location.href); } catch(e) {}
window._updateBackBtn();
};
// panelDrill — push state then run opener (no panel close in between)
window.panelDrill = function(opener, ...args){
window._panelDrilling = true;
try {
window.pushPanelState(opener, args);
const r = opener.apply(null, args);
if(r && typeof r.then === 'function') return r.finally(() => { window._panelDrilling = false; });
return r;
} finally {
window._panelDrilling = false;
}
};
// panelOpen — root entry: clear history then drill
window.panelOpen = function(opener, ...args){
window._panelHistory = [];
return window.panelDrill(opener, ...args);
};
// panelBack — pop current, re-run previous opener
window.panelBack = function(){
if(window._panelHistory.length <= 1){ window.closePanel(); return; }
window._panelHistory.pop();
const prev = window._panelHistory[window._panelHistory.length - 1];
const fn = window._panelOpenerMap[prev.opener];
if(typeof fn !== 'function'){ window.closePanel(); return; }
window._panelSuppressPush = true;
window._panelDrilling = true;
try { fn.apply(null, prev.args || []); } catch(e) { console.error('panelBack', e); }
setTimeout(() => {
window._panelSuppressPush = false;
window._panelDrilling = false;
window._updateBackBtn();
}, 60);
window._updateBackBtn();
};
// Browser back button → mirror panelBack while panel is open
window.addEventListener('popstate', function(){
const panel = document.getElementById('panel');
if(!panel || !panel.classList.contains('open')) return;
if(window._panelHistory.length > 1){
window._panelSuppressPush = true;
window._panelDrilling = true;
window._panelHistory.pop();
const prev = window._panelHistory[window._panelHistory.length - 1];
const fn = window._panelOpenerMap[prev.opener];
if(typeof fn === 'function') fn.apply(null, prev.args || []);
setTimeout(() => {
window._panelSuppressPush = false;
window._panelDrilling = false;
window._updateBackBtn();
}, 60);
window._updateBackBtn();
} else {
window.closePanel();
}
});
// Wrap root open* functions: a non-drill call clears history and registers itself as root.
window._wrapOpener = function(name){
const orig = window[name];
if(typeof orig !== 'function' || orig.__pgzWrapped) return;
window._registerOpener(orig);
const wrapped = function(...args){
if(!window._panelDrilling && !window._panelSuppressPush){
window._panelHistory = [];
window._panelHistory.push({opener: name, args});
try { history.pushState({pgzPanel: true, depth: 1}, '', location.href); } catch(e) {}
window._updateBackBtn();
}
return orig.apply(this, args);
};
wrapped.__pgzWrapped = true;
try { Object.defineProperty(wrapped, 'name', {value: name, configurable: true}); } catch(e) {}
window[name] = wrapped;
window._panelOpenerMap[name] = wrapped;
};
// Wrap all known root openers (idempotent)
// openManif intentionally excluded: uses inline expand, not the side panel
['openSavez','openKlub','openSportas','openObjekt',
'openPrimateljDetail','openProracunDrill','openSavezByName',
'openMrezaNode','openForensicDetail','openOIB']
.forEach(function(n){ try { window._wrapOpener(n); } catch(e) {} });
// Override closePanel — X button always returns to root: clear history & back btn
window._origClosePanel = window.closePanel;
window.closePanel = function(){
window._panelHistory = [];
const back = document.getElementById('panel-back');
if(back) back.style.display = 'none';
const p = document.getElementById('panel');
if(p) p.classList.remove('open');
const ov = document.getElementById('panel-overlay');
if(ov){ ov.classList.remove('open'); ov.style.removeProperty('display'); }
};
// ═══════════════════════════════════════════════════════
// SPORT-S4: Ručni unos financije (admin only)
// ═══════════════════════════════════════════════════════
window._klubsCache = null;
async function loadKlubsForPicker(){
if(window._klubsCache) return window._klubsCache;
try {
const r = await api('/v2/klubovi?limit=2000');
const list = r && (r.results || r.rows || []) || [];
window._klubsCache = list.map(k => ({id:k.id, naziv:k.naziv, sport:k.sport||''}))
.sort((a,b)=>(a.naziv||'').localeCompare(b.naziv||''));
} catch(e){ console.warn('klubovi load fail', e); window._klubsCache = []; }
return window._klubsCache;
}
async function openManualFinancijeForm(){
const klubs = await loadKlubsForPicker();
if(!klubs.length){ alert('Nije moguće učitati popis klubova.'); return; }
const yearNow = new Date().getFullYear();
const yearsOpts = [];
for(let y=yearNow+1; y>=yearNow-10; y--) yearsOpts.push(y);
// Modal overlay
const old = document.getElementById('fi-manual-modal');
if(old) old.remove();
const m = document.createElement('div');
m.id = 'fi-manual-modal';
m.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:9999;display:flex;align-items:center;justify-content:center;padding:14px;font-family:inherit';
m.innerHTML = `
<div style="background:var(--bg,#101018);border:1px solid var(--border,#333);border-radius:10px;padding:22px;max-width:520px;width:100%;max-height:90vh;overflow:auto;color:var(--text,#e8e8f0)">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
<h3 style="margin:0;font-size:17px"> Ručni unos financija (klubu)</h3>
<button onclick="document.getElementById('fi-manual-modal').remove()" style="background:none;border:none;color:#aaa;font-size:24px;cursor:pointer;line-height:1">&times;</button>
</div>
<div style="display:flex;flex-direction:column;gap:10px;font-size:13px">
<label>Godina
<select id="fm-god" style="width:100%;padding:7px;background:var(--surface,#1a1a25);border:1px solid var(--border,#333);color:inherit;border-radius:5px">
${yearsOpts.map(y => '<option value="'+y+'"'+(y===yearNow?' selected':'')+'>'+y+'</option>').join('')}
</select>
</label>
<label>Klub (${klubs.length} u registru)
<input id="fm-klub-q" type="text" placeholder="Pretraži klub po nazivu…" autocomplete="off" style="width:100%;padding:7px;background:var(--surface,#1a1a25);border:1px solid var(--border,#333);color:inherit;border-radius:5px">
<select id="fm-klub" size="6" style="width:100%;margin-top:4px;padding:4px;background:var(--surface,#1a1a25);border:1px solid var(--border,#333);color:inherit;border-radius:5px;font-size:12px">
${klubs.slice(0,200).map(k => '<option value="'+k.id+'">'+esc(k.naziv)+(k.sport?' · '+esc(k.sport):'')+'</option>').join('')}
</select>
</label>
<label>Iznos (€)
<input id="fm-iznos" type="number" step="0.01" min="0.01" placeholder="npr. 12500.00" style="width:100%;padding:7px;background:var(--surface,#1a1a25);border:1px solid var(--border,#333);color:inherit;border-radius:5px">
</label>
<label>Razina
<select id="fm-razina" style="width:100%;padding:7px;background:var(--surface,#1a1a25);border:1px solid var(--border,#333);color:inherit;border-radius:5px">
<option value="ručni_unos" selected>ručni_unos</option>
<option value="županija">županija (PGŽ)</option>
<option value="grad_rijeka">grad_rijeka</option>
<option value="opcina">opcina</option>
<option value="ministarstvo">ministarstvo</option>
</select>
</label>
<label>Vrsta (kratki tag)
<input id="fm-vrsta" type="text" value="ručni_unos" style="width:100%;padding:7px;background:var(--surface,#1a1a25);border:1px solid var(--border,#333);color:inherit;border-radius:5px">
</label>
<label>Opis / Napomena
<textarea id="fm-opis" rows="3" placeholder="npr. ugovor 2025/RSS-117 — Treninzi i natjecanja" style="width:100%;padding:7px;background:var(--surface,#1a1a25);border:1px solid var(--border,#333);color:inherit;border-radius:5px;font-family:inherit"></textarea>
</label>
<label>Source URL (PDF link, opcionalno)
<input id="fm-url" type="url" placeholder="https://…" style="width:100%;padding:7px;background:var(--surface,#1a1a25);border:1px solid var(--border,#333);color:inherit;border-radius:5px">
</label>
<div id="fm-msg" style="font-size:12px;margin-top:4px;min-height:18px"></div>
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:6px">
<button onclick="document.getElementById('fi-manual-modal').remove()" style="padding:8px 14px;background:none;border:1px solid var(--border,#333);color:inherit;border-radius:5px;cursor:pointer">Odustani</button>
<button id="fm-save-btn" onclick="saveManualFinancije()" style="padding:8px 14px;background:var(--pgz-gold,#c5a040);border:none;color:#000;border-radius:5px;cursor:pointer;font-weight:600">💾 Spremi</button>
</div>
</div>
</div>`;
document.body.appendChild(m);
// Live klub filter
const qIn = document.getElementById('fm-klub-q');
const sel = document.getElementById('fm-klub');
qIn.addEventListener('input', () => {
const q = qIn.value.toLowerCase().trim();
const filtered = q
? klubs.filter(k => (k.naziv||'').toLowerCase().includes(q) || (k.sport||'').toLowerCase().includes(q))
: klubs;
sel.innerHTML = filtered.slice(0, 200).map(k =>
'<option value="'+k.id+'">'+esc(k.naziv)+(k.sport?' · '+esc(k.sport):'')+'</option>'
).join('');
if(filtered.length) sel.value = filtered[0].id;
});
}
async function saveManualFinancije(){
const msg = document.getElementById('fm-msg');
const btn = document.getElementById('fm-save-btn');
const payload = {
godina: parseInt(document.getElementById('fm-god').value),
klub_id: parseInt(document.getElementById('fm-klub').value || 0),
iznos_eur: parseFloat(document.getElementById('fm-iznos').value || 0),
razina: document.getElementById('fm-razina').value,
vrsta: (document.getElementById('fm-vrsta').value || 'ručni_unos').trim() || 'ručni_unos',
opis: (document.getElementById('fm-opis').value || '').trim(),
napomena: (document.getElementById('fm-opis').value || '').trim(),
source_url:(document.getElementById('fm-url').value || '').trim() || null,
};
if(!payload.klub_id){ msg.textContent = '❌ Odaberi klub.'; msg.style.color = '#ef4444'; return; }
if(!payload.iznos_eur || payload.iznos_eur <= 0){ msg.textContent = '❌ Iznos mora biti > 0.'; msg.style.color = '#ef4444'; return; }
btn.disabled = true; btn.textContent = 'Spremam…';
msg.style.color = ''; msg.textContent = 'Spremam…';
try {
const r = await fetch('/sport/api/v2/financije/manual-entry', {
method:'POST',
headers:{
'Content-Type':'application/json',
...(window._AUTH_TOKEN ? {'Authorization':'Bearer '+window._AUTH_TOKEN} : {}),
},
body: JSON.stringify(payload),
});
if(!r.ok){
const t = await r.text();
throw new Error('HTTP '+r.status+': '+t.slice(0,200));
}
const d = await r.json();
msg.style.color = '#22c55e';
msg.textContent = '✅ Spremljeno (id='+d.id+'). Tablica se osvježava…';
setTimeout(() => {
const modal = document.getElementById('fi-manual-modal');
if(modal) modal.remove();
if(typeof refreshFinancije === 'function') refreshFinancije();
}, 900);
} catch(e){
msg.style.color = '#ef4444';
msg.textContent = '❌ '+(e.message||e);
btn.disabled = false; btn.textContent = '💾 Spremi';
}
}
</script>
<script src="/static/js/export_dropdown.js"></script>
<script src="/static/_ai_widget.js" defer></script>
</body>
</html>