4ecd7fafa3
- Klub naziv → openKlub(klub_id) on click - Sport, mjesto rođenja, datum (godina) → cross-section filter - HOO / REPR / AKTIVAN / STIP badges → clickable filters - OIB → opens sudreg.pravosudje.hr lookup - New helpers: filterSportasiBy, filterSportasiByYear, filterKluboviByCity/Sport, filterObjektiByCity, openOIB - New CSS .link-chip for inline cyan→gold underlined chips Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2343 lines
116 KiB
HTML
2343 lines
116 KiB
HTML
<!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>
|
||
<style>
|
||
:root{
|
||
--pgz-blue:#003087; --pgz-blue2:#004CC4; --pgz-gold:#F4C430;
|
||
--bg0:#08090e; --bg1:#0d1021; --bg2:#111628; --bg3:#161d35; --bg4:#1c2542;
|
||
--rim:#1e2a50; --rim2:#283560;
|
||
--t0:#fff; --t1:#e2e6f0; --t2:#8a95b4; --t4:#4e5a7a;
|
||
--green:#00e88f; --red:#ff2d55; --amber:#f59e0b; --cyan:#00c8e8;
|
||
--font:'Inter',sans-serif; --mono:'JetBrains Mono',monospace;
|
||
}
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
html,body{height:100%}
|
||
body{font-family:var(--font);background:var(--bg0);color:var(--t1);font-size:13px;overflow-x:hidden}
|
||
a{color:var(--cyan);text-decoration:none}
|
||
a:hover{color:var(--pgz-gold)}
|
||
button,input,select{font-family:inherit;font-size:inherit;outline:none}
|
||
::-webkit-scrollbar{width:8px;height:8px}
|
||
::-webkit-scrollbar-track{background:var(--bg1)}
|
||
::-webkit-scrollbar-thumb{background:var(--rim2);border-radius:4px}
|
||
::-webkit-scrollbar-thumb:hover{background:var(--pgz-blue2)}
|
||
|
||
.app{display:flex;min-height:100vh}
|
||
.sb{width:240px;background:linear-gradient(180deg,var(--bg1) 0%,var(--bg0) 100%);border-right:1px solid var(--rim);position:fixed;top:0;left:0;bottom:0;display:flex;flex-direction:column;z-index:10;transition:width .22s ease}
|
||
.sb-h{padding:18px 18px 14px;border-bottom:1px solid var(--rim);position:relative}
|
||
.sb-h .logo{font-weight:800;font-size:14px;color:var(--t0);letter-spacing:.5px;white-space:nowrap;overflow:hidden}
|
||
.sb-h .logo .g{color:var(--pgz-gold)}
|
||
.sb-h .sub{font-size:10px;color:var(--t2);margin-top:4px;text-transform:uppercase;letter-spacing:1px;white-space:nowrap;overflow:hidden}
|
||
.sb-toggle{position:absolute;top:14px;right:8px;width:22px;height:22px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--t2);background:var(--bg2);border:1px solid var(--rim);border-radius:4px;font-size:11px;font-weight:700;transition:all .15s;user-select:none}
|
||
.sb-toggle:hover{background:var(--bg3);color:var(--pgz-gold);border-color:var(--pgz-gold)}
|
||
.sb-nav{flex:1;padding:10px 8px;overflow-y:auto;overflow-x:hidden}
|
||
.nav-i{padding:9px 12px;border-radius:6px;color:var(--t2);cursor:pointer;display:flex;align-items:center;gap:10px;font-size:12.5px;margin-bottom:2px;transition:background .15s,color .15s;white-space:nowrap}
|
||
.nav-i:hover{background:var(--bg2);color:var(--t1)}
|
||
.nav-i.active{background:linear-gradient(90deg,var(--pgz-blue) 0%,var(--pgz-blue2) 100%);color:#fff;font-weight:600}
|
||
.nav-i .ic{width:18px;text-align:center;font-size:14px;flex-shrink:0}
|
||
.nav-i .lbl{overflow:hidden;text-overflow:ellipsis}
|
||
.sb-foot{padding:10px 14px;border-top:1px solid var(--rim);font-size:10px;color:var(--t4);white-space:nowrap;overflow:hidden}
|
||
|
||
/* Collapsed sidebar */
|
||
.sb.collapsed{width:58px}
|
||
.sb.collapsed .sb-h{padding:18px 8px 14px;text-align:center}
|
||
.sb.collapsed .sb-h .logo{font-size:0}
|
||
.sb.collapsed .sb-h .logo::before{content:"PG";font-size:13px;color:var(--pgz-gold);font-weight:800}
|
||
.sb.collapsed .sb-h .sub{display:none}
|
||
.sb.collapsed .sb-toggle{position:static;margin:6px auto 0;display:flex}
|
||
.sb.collapsed .nav-i{justify-content:center;padding:10px 6px}
|
||
.sb.collapsed .nav-i .lbl{display:none}
|
||
.sb.collapsed .nav-i{position:relative}
|
||
.sb.collapsed .nav-i:hover::after{content:attr(data-label);position:absolute;left:58px;top:50%;transform:translateY(-50%);background:var(--bg3);color:var(--t0);padding:5px 10px;border-radius:4px;font-size:11.5px;white-space:nowrap;border:1px solid var(--rim);z-index:50;font-weight:600;pointer-events:none;box-shadow:2px 2px 8px rgba(0,0,0,.4)}
|
||
.sb.collapsed .sb-foot{font-size:0;padding:8px}
|
||
.sb.collapsed .sb-foot::before{content:"v2";font-size:9px;color:var(--t4)}
|
||
|
||
.main{margin-left:240px;flex:1;min-width:0;transition:margin-left .22s ease}
|
||
.sb.collapsed ~ .main{margin-left:58px}
|
||
.tb{background:var(--bg1);border-bottom:1px solid var(--rim);padding:12px 22px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:5}
|
||
.tb-t{font-size:15px;font-weight:700;color:var(--t0)}
|
||
.tb-s{font-size:11px;color:var(--t2)}
|
||
.content{padding:22px}
|
||
.section{display:none}
|
||
.section.active{display:block}
|
||
|
||
.kpi-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px;margin-bottom:22px}
|
||
.kpi{background:linear-gradient(135deg,var(--bg2) 0%,var(--bg1) 100%);border:1px solid var(--rim);border-radius:8px;padding:14px 16px;position:relative;overflow:hidden}
|
||
.kpi::before{content:"";position:absolute;top:0;left:0;width:3px;height:100%;background:var(--pgz-gold)}
|
||
.kpi.b::before{background:var(--pgz-blue2)}
|
||
.kpi.g::before{background:var(--green)}
|
||
.kpi.r::before{background:var(--red)}
|
||
.kpi-l{font-size:10.5px;color:var(--t2);text-transform:uppercase;letter-spacing:1px;font-weight:600}
|
||
.kpi-v{font-size:24px;font-weight:800;color:var(--t0);margin-top:4px;font-family:var(--mono)}
|
||
.kpi-s{font-size:10px;color:var(--t4);margin-top:2px}
|
||
|
||
.card{background:var(--bg2);border:1px solid var(--rim);border-radius:8px;padding:14px;margin-bottom:14px}
|
||
.card-h{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--rim)}
|
||
.card-t{font-weight:700;color:var(--t0);font-size:13px}
|
||
|
||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px}
|
||
.grid-club{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}
|
||
.grid-player{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px}
|
||
|
||
.entity{background:var(--bg2);border:1px solid var(--rim);border-radius:8px;padding:12px;cursor:pointer;transition:all .2s;position:relative}
|
||
.entity:hover{border-color:var(--pgz-gold);background:var(--bg3);transform:translateY(-1px)}
|
||
.entity .et{font-weight:700;color:var(--t0);font-size:13px;margin-bottom:6px;line-height:1.3}
|
||
.entity .es{font-size:11px;color:var(--t2);margin-bottom:6px}
|
||
.entity .em{display:flex;gap:8px;font-size:10.5px;color:var(--t4);flex-wrap:wrap}
|
||
.entity .em b{color:var(--pgz-gold);font-weight:700}
|
||
.entity .et-tag{position:absolute;top:8px;right:8px;font-size:9px;padding:2px 6px;border-radius:3px;background:var(--pgz-blue);color:#fff;font-weight:600;text-transform:uppercase}
|
||
|
||
.player-card{background:var(--bg2);border:1px solid var(--rim);border-radius:8px;overflow:hidden;cursor:pointer;transition:all .2s}
|
||
.player-card:hover{border-color:var(--pgz-gold);transform:translateY(-1px)}
|
||
.player-card .ph{height:120px;background:linear-gradient(135deg,var(--bg3),var(--bg2));position:relative;overflow:hidden;display:flex;align-items:center;justify-content:center}
|
||
.player-card .ph img{width:100%;height:100%;object-fit:cover;object-position:top}
|
||
.player-card .ph .no{font-size:36px;color:var(--t4);font-weight:800}
|
||
.player-card .pb{padding:10px}
|
||
.player-card .pn{font-weight:700;color:var(--t0);font-size:13px;line-height:1.2}
|
||
.player-card .pp{font-size:11px;color:var(--t2);margin-top:3px}
|
||
.player-card .pk{font-size:10.5px;color:var(--t4);margin-top:3px}
|
||
.player-card .badges{display:flex;gap:4px;margin-top:6px;flex-wrap:wrap}
|
||
.player-card .badge{font-size:9px;padding:2px 5px;border-radius:3px;background:var(--bg4);color:var(--t1);text-transform:uppercase;font-weight:600}
|
||
.player-card .badge.repr{background:var(--pgz-gold);color:var(--bg0)}
|
||
.player-card .badge.hoo{background:var(--pgz-blue2);color:#fff}
|
||
|
||
table{width:100%;border-collapse:collapse;font-size:12px}
|
||
table th{background:var(--bg3);color:var(--t2);text-transform:uppercase;font-size:10px;letter-spacing:.5px;padding:8px 10px;text-align:left;border-bottom:1px solid var(--rim);font-weight:700}
|
||
table td{padding:8px 10px;border-bottom:1px solid var(--rim);color:var(--t1)}
|
||
table tbody tr{cursor:pointer;transition:background .15s}
|
||
table tbody tr:hover{background:var(--bg3)}
|
||
table tbody tr.no-click{cursor:default}
|
||
table tbody tr.no-click:hover{background:transparent}
|
||
.num{font-family:var(--mono);text-align:right}
|
||
|
||
.toolbar{display:flex;align-items:center;gap:10px;margin-bottom:14px;flex-wrap:wrap}
|
||
.toolbar input,.toolbar select{background:var(--bg2);border:1px solid var(--rim);border-radius:5px;padding:7px 10px;color:var(--t1);font-size:12px}
|
||
.toolbar input:focus,.toolbar select:focus{border-color:var(--pgz-blue2)}
|
||
.toolbar input{min-width:200px}
|
||
.toolbar label{font-size:11px;color:var(--t2);display:flex;align-items:center;gap:6px}
|
||
.toolbar input[type=checkbox]{accent-color:var(--pgz-gold)}
|
||
.btn{background:var(--bg2);border:1px solid var(--rim);border-radius:5px;padding:7px 12px;color:var(--t1);font-size:12px;cursor:pointer;font-weight:600;transition:all .15s}
|
||
.btn:hover{background:var(--bg3);border-color:var(--rim2)}
|
||
.btn.primary{background:linear-gradient(135deg,var(--pgz-blue),var(--pgz-blue2));border-color:transparent;color:#fff}
|
||
.btn.primary:hover{filter:brightness(1.1)}
|
||
|
||
.toggle{display:inline-flex;background:var(--bg2);border:1px solid var(--rim);border-radius:5px;overflow:hidden}
|
||
.toggle button{background:transparent;border:0;padding:6px 12px;color:var(--t2);font-size:11px;font-weight:600;cursor:pointer}
|
||
.toggle button.active{background:var(--pgz-blue);color:#fff}
|
||
|
||
.tabs{display:flex;gap:4px;border-bottom:1px solid var(--rim);margin-bottom:14px;flex-wrap:wrap}
|
||
.tab{padding:8px 14px;cursor:pointer;color:var(--t2);font-weight:600;font-size:12px;border-bottom:2px solid transparent;transition:all .15s}
|
||
.tab:hover{color:var(--t1)}
|
||
.tab.active{color:var(--pgz-gold);border-color:var(--pgz-gold)}
|
||
|
||
.empty{text-align:center;padding:30px;color:var(--t4);font-size:12px;font-style:italic}
|
||
.loading{padding:24px;text-align:center;color:var(--t2);font-size:12px}
|
||
.loading::before{content:"";display:inline-block;width:12px;height:12px;border:2px solid var(--rim);border-top-color:var(--pgz-gold);border-radius:50%;animation:spin .8s linear infinite;margin-right:8px;vertical-align:middle}
|
||
@keyframes spin{to{transform:rotate(360deg)}}
|
||
|
||
.tag{display:inline-block;padding:2px 7px;font-size:10px;border-radius:3px;background:var(--bg4);color:var(--t1);font-weight:600;text-transform:uppercase;letter-spacing:.5px;margin-right:3px}
|
||
a.tag,.tag[onclick]{cursor:pointer;text-decoration:none;transition:transform .12s,filter .12s}
|
||
a.tag:hover,.tag[onclick]:hover{transform:translateY(-1px);filter:brightness(1.15)}
|
||
.link-chip{color:var(--cyan);cursor:pointer;text-decoration:none;border-bottom:1px dashed transparent;transition:all .15s}
|
||
.link-chip:hover{color:var(--pgz-gold);border-bottom-color:var(--pgz-gold)}
|
||
.kv .v a{color:var(--cyan)}
|
||
.kv .v a:hover{color:var(--pgz-gold)}
|
||
.tag.b{background:var(--pgz-blue);color:#fff}
|
||
.tag.gd{background:var(--pgz-gold);color:var(--bg0)}
|
||
.tag.gr{background:var(--green);color:var(--bg0)}
|
||
.tag.rd{background:var(--red);color:#fff}
|
||
.tag.am{background:var(--amber);color:var(--bg0)}
|
||
|
||
#panel{position:fixed;top:0;right:-620px;width:600px;max-width:96vw;height:100vh;background:var(--bg1);border-left:1px solid var(--rim);z-index:200;transition:right .25s ease;display:flex;flex-direction:column;box-shadow:-8px 0 30px rgba(0,0,0,.5)}
|
||
#panel.open{right:0}
|
||
#panel-hdr{padding:14px 16px;border-bottom:1px solid var(--rim);display:flex;align-items:center;justify-content:space-between;flex-shrink:0;background:var(--bg2)}
|
||
#panel-hdr-t{font-size:14px;font-weight:700;color:var(--t0)}
|
||
#panel-x{cursor:pointer;font-size:22px;color:var(--t4);width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:5px;transition:all .15s}
|
||
#panel-x:hover{background:var(--bg3);color:var(--red)}
|
||
#panel-body{flex:1;overflow-y:auto;padding:16px}
|
||
#panel-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:199;backdrop-filter:blur(2px)}
|
||
#panel-overlay.open{display:block}
|
||
|
||
.pp-hdr{display:flex;gap:14px;align-items:flex-start;margin-bottom:14px;padding-bottom:14px;border-bottom:1px solid var(--rim)}
|
||
.pp-foto{width:90px;height:110px;border-radius:6px;background:var(--bg3);overflow:hidden;flex-shrink:0;display:flex;align-items:center;justify-content:center}
|
||
.pp-foto img{width:100%;height:100%;object-fit:cover;object-position:top}
|
||
.pp-foto .no{font-size:32px;color:var(--t4);font-weight:800}
|
||
.pp-info{flex:1;min-width:0}
|
||
.pp-name{font-size:18px;font-weight:800;color:var(--t0);margin-bottom:4px}
|
||
.pp-meta{font-size:12px;color:var(--t2);margin-bottom:6px}
|
||
.pp-tags{display:flex;gap:4px;flex-wrap:wrap}
|
||
.pp-stats{display:grid;grid-template-columns:repeat(6,1fr);gap:8px;margin-bottom:14px;padding:12px;background:var(--bg2);border:1px solid var(--rim);border-radius:6px}
|
||
.pp-stat{text-align:center}
|
||
.pp-stat .v{font-size:20px;font-weight:800;color:var(--pgz-gold);font-family:var(--mono)}
|
||
.pp-stat .l{font-size:9px;color:var(--t4);text-transform:uppercase;letter-spacing:.5px;margin-top:2px;font-weight:600}
|
||
|
||
.kv{display:grid;grid-template-columns:140px 1fr;gap:6px 12px;font-size:12px}
|
||
.kv .k{color:var(--t2);font-weight:600}
|
||
.kv .v{color:var(--t1);word-break:break-word}
|
||
|
||
.utlogo{width:22px;height:22px;border-radius:50%;background:var(--bg3);object-fit:contain;vertical-align:middle;margin-right:6px}
|
||
|
||
.score{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:4px;background:var(--bg3);font-size:11px;font-weight:600}
|
||
.score.high{background:rgba(0,232,143,.15);color:var(--green)}
|
||
.score.mid{background:rgba(245,158,11,.15);color:var(--amber)}
|
||
.score.low{background:rgba(255,45,85,.15);color:var(--red)}
|
||
|
||
.row-2{display:grid;grid-template-columns:1fr 1fr;gap:14px}
|
||
@media (max-width:900px){.row-2{grid-template-columns:1fr}}
|
||
|
||
.chart-box{background:var(--bg2);border:1px solid var(--rim);border-radius:8px;padding:14px;height:340px}
|
||
.chart-box canvas{max-height:300px}
|
||
|
||
.alert-card{padding:10px 12px;border-left:3px solid var(--amber);background:var(--bg2);border-radius:5px;margin-bottom:8px}
|
||
.alert-card.crit{border-color:var(--red)}
|
||
.alert-card .at{font-weight:700;font-size:12px;color:var(--t0)}
|
||
.alert-card .ad{font-size:11px;color:var(--t2);margin-top:3px}
|
||
|
||
.iframe-map{width:100%;height:140px;border:0;border-radius:5px;background:var(--bg3)}
|
||
|
||
@media (max-width:768px){
|
||
.sb{transform:translateX(-100%);transition:transform .25s}
|
||
.sb.open{transform:translateX(0)}
|
||
.main{margin-left:0}
|
||
.pp-stats{grid-template-columns:repeat(3,1fr)}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="app">
|
||
<aside class="sb" id="sb">
|
||
<div class="sb-h">
|
||
<div class="logo">PGŽ <span class="g">SPORT</span></div>
|
||
<div class="sub">Primorsko-goranska županija</div>
|
||
<div class="sb-toggle" id="sb-toggle" onclick="toggleSidebar()" title="Skupi/raširi">⮜</div>
|
||
</div>
|
||
<nav class="sb-nav" id="nav"></nav>
|
||
<div class="sb-foot">v2.0 · 2026</div>
|
||
</aside>
|
||
|
||
<main class="main">
|
||
<div class="tb">
|
||
<div>
|
||
<div class="tb-t" id="tb-t">Dashboard</div>
|
||
<div class="tb-s" id="tb-s">Pregled stanja</div>
|
||
</div>
|
||
<div class="tb-s">
|
||
<span style="color:var(--green)">●</span> API live · sport.rinet.one
|
||
</div>
|
||
</div>
|
||
|
||
<div class="content">
|
||
<section id="pg-dashboard" class="section active"></section>
|
||
<section id="pg-savezi" class="section"></section>
|
||
<section id="pg-klubovi" class="section"></section>
|
||
<section id="pg-sportasi" class="section"></section>
|
||
<section id="pg-financije" class="section"></section>
|
||
<section id="pg-objekti" class="section"></section>
|
||
<section id="pg-manifestacije" class="section"></section>
|
||
<section id="pg-mreza" class="section"></section>
|
||
<section id="pg-forenzika" class="section"></section>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<div id="panel-overlay" onclick="closePanel()"></div>
|
||
<div id="panel">
|
||
<div id="panel-hdr">
|
||
<div id="panel-hdr-t">Detalji</div>
|
||
<div id="panel-x" onclick="closePanel()">×</div>
|
||
</div>
|
||
<div id="panel-body"></div>
|
||
</div>
|
||
|
||
<script>
|
||
//=========== CONFIG ===========
|
||
const API = '/sport/api';
|
||
const NAV_ITEMS = [
|
||
{id:'dashboard', ic:'\u{1F4CA}', label:'Dashboard'},
|
||
{id:'savezi', ic:'\u{1F3C5}', label:'Savezi'},
|
||
{id:'klubovi', ic:'⬢', label:'Klubovi'},
|
||
{id:'sportasi', ic:'\u{1F464}', label:'Sportaši'},
|
||
{id:'financije', ic:'€', label:'Financije'},
|
||
{id:'objekti', ic:'\u{1F4CD}', label:'Objekti'},
|
||
{id:'manifestacije', ic:'\u{1F4C5}', label:'Manifestacije'},
|
||
{id:'mreza', ic:'\u{1F578}', label:'Mreža'},
|
||
{id:'forenzika', ic:'⚠', label:'Forenzika'}
|
||
];
|
||
const SECTION_TITLES = {
|
||
dashboard: ['Dashboard', 'Pregled stanja PGŽ Sporta'],
|
||
savezi: ['Savezi', '246 sportskih saveza'],
|
||
klubovi: ['Klubovi', 'Sportski klubovi PGŽ'],
|
||
sportasi: ['Sportaši', 'Registrirani članovi'],
|
||
financije: ['Financije', 'Sufinanciranje sporta'],
|
||
objekti: ['Sportski objekti', 'Geocodirana infrastruktura'],
|
||
manifestacije: ['Manifestacije', 'Sportski događaji'],
|
||
mreza: ['Mreža', 'Force-directed graf entiteta i veza'],
|
||
forenzika: ['Forenzika', 'Kritični nalazi i alarmi']
|
||
};
|
||
|
||
const _cache = {savezi:null, klubovi:null, clanovi:null, objekti:null, manifestacije:null, sufin:null, dash:null};
|
||
const _state = {section:'dashboard', viewSavezi:'card', viewKlubovi:'card', viewSportasi:'card', viewObjekti:'card', viewManif:'card', viewFinancije:'table'};
|
||
const _sort = {savezi:null, klubovi:null, sportasi:null, objekti:null, manifestacije:null, financije:null};
|
||
let _proracunChart=null, _financijeChart=null;
|
||
|
||
//=========== UTIL ===========
|
||
const $ = s => document.querySelector(s);
|
||
const $$ = s => Array.from(document.querySelectorAll(s));
|
||
function esc(s){
|
||
if(s===null||s===undefined) return '';
|
||
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
}
|
||
function fmtNum(n){
|
||
if(n===null||n===undefined||n==='') return '—';
|
||
const v = Number(n);
|
||
if(isNaN(v)) return esc(n);
|
||
return v.toLocaleString('hr-HR');
|
||
}
|
||
function fmtEur(n){
|
||
if(n===null||n===undefined||n==='') return '—';
|
||
const v = Number(n);
|
||
if(isNaN(v)) return esc(n);
|
||
if(v>=1000000) return '€'+(v/1000000).toFixed(2)+'M';
|
||
if(v>=1000) return '€'+(v/1000).toFixed(1)+'k';
|
||
return '€'+v.toFixed(0);
|
||
}
|
||
function fmtEurFull(n){
|
||
if(n===null||n===undefined||n==='') return '—';
|
||
const v = Number(n);
|
||
if(isNaN(v)) return esc(n);
|
||
return v.toLocaleString('hr-HR', {minimumFractionDigits:2, maximumFractionDigits:2})+' €';
|
||
}
|
||
function fmtDate(s){
|
||
if(!s) return '—';
|
||
const m = String(s).match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||
if(!m) return esc(s);
|
||
return m[3]+'.'+m[2]+'.'+m[1];
|
||
}
|
||
function txt(v, fb){
|
||
if(v===null||v===undefined||v==='') return fb===undefined?'—':fb;
|
||
return esc(v);
|
||
}
|
||
async function api(path){
|
||
try{
|
||
const r = await fetch(API+path);
|
||
if(!r.ok) throw new Error('HTTP '+r.status);
|
||
return await r.json();
|
||
}catch(e){
|
||
console.error('API error', path, e);
|
||
return null;
|
||
}
|
||
}
|
||
async function apiPost(path, body){
|
||
try{
|
||
const r = await fetch(API+path, {method:'POST', headers:{'Content-Type':'application/json'}, body: body?JSON.stringify(body):'{}'});
|
||
if(!r.ok) throw new Error('HTTP '+r.status);
|
||
return await r.json();
|
||
}catch(e){
|
||
console.error('API POST error', path, e);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function enrichEntity(kind, id){
|
||
const targetId = 'enrich-out-'+kind+'-'+id;
|
||
const target = document.getElementById(targetId);
|
||
if(target) target.innerHTML = '<div class="loading">⏳ Obogaćivanje u tijeku — pretraživanje izvora…</div>';
|
||
const r = await apiPost('/v2/enrich/'+kind+'/'+id);
|
||
if(!r){ if(target) target.innerHTML = '<div class="empty">Greška pri obogaćivanju</div>'; return; }
|
||
const cov = r.coverage||0;
|
||
const covCls = cov>=70?'high':(cov>=40?'mid':'low');
|
||
const html = `
|
||
<div style="display:flex;gap:8px;align-items:center;margin-bottom:10px">
|
||
<span class="tag gr">🟢 OBOGAĆENO</span>
|
||
<span class="score ${covCls}">Coverage ${cov}%</span>
|
||
<span class="tb-s">${r.filled_fields}/${r.total_fields} polja popunjeno</span>
|
||
</div>
|
||
${r.live_snippet && r.live_snippet.title ? `
|
||
<div style="padding:10px;background:var(--bg3);border-left:3px solid var(--pgz-gold);border-radius:5px;margin-bottom:10px">
|
||
<div style="font-size:11px;color:var(--t4);text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px">📡 Live snippet</div>
|
||
<div style="font-weight:700;color:var(--t0);font-size:13px;margin-bottom:4px">${esc(r.live_snippet.title)}</div>
|
||
${r.live_snippet.description ? '<div style="font-size:11.5px;color:var(--t1);line-height:1.5">'+esc(r.live_snippet.description)+'</div>' : ''}
|
||
<div style="margin-top:6px"><a href="${esc(r.live_snippet.url)}" target="_blank">↗ ${esc(r.live_snippet.url.slice(0,80))}</a></div>
|
||
</div>
|
||
` : ''}
|
||
${r.missing_fields && r.missing_fields.length ? `
|
||
<div style="margin-bottom:10px">
|
||
<div style="font-size:11px;color:var(--t2);margin-bottom:4px">Nedostaje:</div>
|
||
<div>${r.missing_fields.map(f=>'<span class="tag rd">'+esc(f)+'</span>').join('')}</div>
|
||
</div>
|
||
` : ''}
|
||
<div>
|
||
<div style="font-size:11px;color:var(--t2);margin-bottom:6px">🔍 Istraži dalje:</div>
|
||
<div style="display:flex;flex-wrap:wrap;gap:6px">
|
||
${(r.research_links||[]).map(l => '<a href="'+esc(l.url)+'" target="_blank" class="btn">'+l.icon+' '+esc(l.label)+'</a>').join('')}
|
||
</div>
|
||
</div>
|
||
`;
|
||
if(target) target.innerHTML = html;
|
||
}
|
||
|
||
function enrichBlock(kind, id){
|
||
return `
|
||
<div class="card" id="enrich-card-${kind}-${id}">
|
||
<div class="card-h">
|
||
<div class="card-t">✨ Obogati podatke</div>
|
||
<button class="btn primary" onclick="enrichEntity('${kind}',${id})">▶ Pokreni</button>
|
||
</div>
|
||
<div id="enrich-out-${kind}-${id}">
|
||
<div class="empty" style="padding:14px">Klikom na "Pokreni" platforma će pretražiti vanjske izvore (Google, Wikipedia, službene web stranice) i prikazati dopune za ovu entitetsku karticu.</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
function sortRows(rows, key, dir){
|
||
if(!key) return rows;
|
||
const sorted = rows.slice();
|
||
sorted.sort((a,b) => {
|
||
let av = a[key], bv = b[key];
|
||
if(av==null && bv==null) return 0;
|
||
if(av==null) return 1;
|
||
if(bv==null) return -1;
|
||
const an = typeof av === 'number' ? av : Number(av);
|
||
const bn = typeof bv === 'number' ? bv : Number(bv);
|
||
if(!isNaN(an) && !isNaN(bn) && (typeof av === 'number' || /^[-+]?\d+(\.\d+)?$/.test(String(av)))){
|
||
return dir==='asc' ? (an-bn) : (bn-an);
|
||
}
|
||
const cmp = String(av).localeCompare(String(bv), 'hr', {numeric:true});
|
||
return dir==='asc' ? cmp : -cmp;
|
||
});
|
||
return sorted;
|
||
}
|
||
function setSort(section, key){
|
||
const cur = _sort[section];
|
||
if(cur && cur.key === key){
|
||
_sort[section] = {key, dir: cur.dir==='asc' ? 'desc' : 'asc'};
|
||
} else {
|
||
_sort[section] = {key, dir: 'asc'};
|
||
}
|
||
// Re-render
|
||
switch(section){
|
||
case 'savezi': return applySaveziFilter();
|
||
case 'klubovi': return applyKluboviFilter();
|
||
case 'sportasi': return applySportasiFilter();
|
||
case 'objekti': return applyObjektiFilter();
|
||
case 'manifestacije': return applyManifFilter();
|
||
case 'financije': return refreshFinancije();
|
||
}
|
||
}
|
||
function sortHeader(section, key, label, klass){
|
||
const s = _sort[section];
|
||
const isCur = s && s.key === key;
|
||
const arrow = isCur ? (s.dir==='asc' ? ' ▲' : ' ▼') : '';
|
||
const cls = (klass||'') + ' sortable';
|
||
return '<th class="'+cls+'" onclick="setSort(\''+section+'\',\''+key+'\')" style="cursor:pointer;user-select:none">'+esc(label)+'<span style="color:var(--pgz-gold);font-weight:700">'+arrow+'</span></th>';
|
||
}
|
||
function debounce(fn, ms){
|
||
let t;
|
||
return function(){
|
||
const args = arguments;
|
||
clearTimeout(t);
|
||
t = setTimeout(()=>fn.apply(null, args), ms);
|
||
};
|
||
}
|
||
|
||
//=========== PANEL ===========
|
||
function openPanel(title, html){
|
||
const t = $('#panel-hdr-t');
|
||
if(t) t.textContent = title || 'Detalji';
|
||
const b = $('#panel-body');
|
||
if(b) b.innerHTML = html;
|
||
$('#panel').classList.add('open');
|
||
$('#panel-overlay').classList.add('open');
|
||
}
|
||
function closePanel(){
|
||
$('#panel').classList.remove('open');
|
||
$('#panel-overlay').classList.remove('open');
|
||
}
|
||
document.addEventListener('keydown', e => { if(e.key==='Escape') closePanel(); });
|
||
|
||
//=========== NAVIGATION ===========
|
||
function buildNav(){
|
||
const nav = $('#nav');
|
||
nav.innerHTML = NAV_ITEMS.map(n => '<div class="nav-i '+(n.id===_state.section?'active':'')+'" data-id="'+n.id+'" data-label="'+n.label+'" onclick="navTo(\''+n.id+'\')"><span class="ic">'+n.ic+'</span><span class="lbl">'+n.label+'</span></div>').join('');
|
||
}
|
||
function toggleSidebar(){
|
||
const sb = document.getElementById('sb');
|
||
const tg = document.getElementById('sb-toggle');
|
||
if(!sb) return;
|
||
const isCollapsed = sb.classList.toggle('collapsed');
|
||
if(tg) tg.textContent = isCollapsed ? '⮞' : '⮜';
|
||
try { localStorage.setItem('sidebar-state', isCollapsed ? 'collapsed' : 'expanded'); } catch(e){}
|
||
}
|
||
function restoreSidebar(){
|
||
try {
|
||
const s = localStorage.getItem('sidebar-state');
|
||
if(s === 'collapsed'){
|
||
const sb = document.getElementById('sb');
|
||
const tg = document.getElementById('sb-toggle');
|
||
if(sb) sb.classList.add('collapsed');
|
||
if(tg) tg.textContent = '⮞';
|
||
}
|
||
} catch(e){}
|
||
}
|
||
function navTo(id){
|
||
_state.section = id;
|
||
$$('.nav-i').forEach(el => el.classList.toggle('active', el.dataset.id===id));
|
||
$$('.section').forEach(el => el.classList.remove('active'));
|
||
const sec = $('#pg-'+id);
|
||
if(sec) sec.classList.add('active');
|
||
const t = SECTION_TITLES[id] || [id, ''];
|
||
$('#tb-t').textContent = t[0];
|
||
$('#tb-s').textContent = t[1];
|
||
loadSection(id);
|
||
}
|
||
function loadSection(id){
|
||
switch(id){
|
||
case 'dashboard': return loadDash();
|
||
case 'savezi': return loadSavezi();
|
||
case 'klubovi': return loadKlubovi();
|
||
case 'sportasi': return loadSportasi();
|
||
case 'financije': return loadFinancije();
|
||
case 'objekti': return loadObjekti();
|
||
case 'manifestacije': return loadManifestacije();
|
||
case 'mreza': return loadMreza();
|
||
case 'forenzika': return loadForenzika();
|
||
}
|
||
}
|
||
|
||
//=========== DASHBOARD ===========
|
||
async function loadDash(){
|
||
const root = $('#pg-dashboard');
|
||
root.innerHTML = '<div class="loading">Učitavanje dashboard podataka…</div>';
|
||
const d = await api('/dashboard');
|
||
if(!d){ root.innerHTML='<div class="empty">Greška pri dohvatu podataka</div>'; return; }
|
||
_cache.dash = d;
|
||
const proracun2026 = (d.proracun_trend||[]).filter(x=>x.godina===2026)[0];
|
||
const total2026 = proracun2026 ? proracun2026.ukupno : d.proracun_aktualni;
|
||
|
||
root.innerHTML = `
|
||
<div class="kpi-grid">
|
||
<div class="kpi"><div class="kpi-l">Saveza</div><div class="kpi-v">${fmtNum(d.aktivnih_saveza)}</div><div class="kpi-s">aktivnih</div></div>
|
||
<div class="kpi b"><div class="kpi-l">Klubova</div><div class="kpi-v">${fmtNum(d.aktivnih_klubova)}</div><div class="kpi-s">${d.nositelja_kvalitete||0} nositelja kvalitete</div></div>
|
||
<div class="kpi g"><div class="kpi-l">Sportaša</div><div class="kpi-v">${fmtNum(d.aktivnih_clanova)}</div><div class="kpi-s">${d.reprezentativaca||0} reprezentativaca</div></div>
|
||
<div class="kpi"><div class="kpi-l">Proračun ${proracun2026?proracun2026.godina:2026}</div><div class="kpi-v">${fmtEur(total2026)}</div><div class="kpi-s">javne potrebe u sportu</div></div>
|
||
<div class="kpi r"><div class="kpi-l">Kritičnih alarma</div><div class="kpi-v">${fmtNum(d.critical_alerts)}</div><div class="kpi-s">${d.warning_alerts||0} upozorenja</div></div>
|
||
</div>
|
||
|
||
<div class="row-2">
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">📈 Proračun za sport po godinama</div><div class="tb-s">Klikni godinu za detalje (PDF dokumenti)</div></div>
|
||
<div class="chart-box"><canvas id="chProracun"></canvas></div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">🏅 Top savezi po broju registriranih</div><div class="tb-s">Klikni za drill-down</div></div>
|
||
<div style="overflow-x:auto;max-height:340px;overflow-y:auto"><table>
|
||
<thead><tr><th>Savez</th><th class="num">Klubova</th><th class="num">Reg.</th><th class="num">Trenera</th><th class="num">Repr.</th></tr></thead>
|
||
<tbody>${(d.top_savezi||[]).slice(0,10).map(s => `
|
||
<tr onclick="openSavezByName('${esc(s.naziv).replace(/'/g,"\\'")}')">
|
||
<td><b>${esc(s.naziv)}</b></td>
|
||
<td class="num">${fmtNum(s.klubova_clanica)}</td>
|
||
<td class="num">${fmtNum(s.registriranih)}</td>
|
||
<td class="num">${fmtNum(s.trenera)}</td>
|
||
<td class="num">${fmtNum(s.reprezentativaca)}</td>
|
||
</tr>`).join('')}</tbody>
|
||
</table></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card" style="margin-top:14px">
|
||
<div class="card-h">
|
||
<div class="card-t">💰 Najveći primatelji javnih potreba</div>
|
||
<div style="display:flex;gap:8px;align-items:center">
|
||
<select id="dash-god" onchange="refreshDashNositelji()" style="background:var(--bg2);border:1px solid var(--rim);border-radius:5px;padding:6px 10px;color:var(--t1);font-size:12px">
|
||
<option value="2026">2026</option>
|
||
<option value="2025" selected>2025</option>
|
||
<option value="2024">2024</option>
|
||
</select>
|
||
<span class="tb-s" id="dash-nos-cnt"></span>
|
||
</div>
|
||
</div>
|
||
<div id="dash-nos-out"><div class="loading">Učitavanje…</div></div>
|
||
</div>
|
||
`;
|
||
drawProracunChart(d.proracun_trend || []);
|
||
refreshDashNositelji();
|
||
}
|
||
|
||
async function refreshDashNositelji(){
|
||
const sel = $('#dash-god');
|
||
if(!sel) return;
|
||
const god = sel.value;
|
||
const out = $('#dash-nos-out');
|
||
out.innerHTML = '<div class="loading">Učitavanje primatelja '+god+'…</div>';
|
||
const d = await api('/v2/potpore/by-year?godina='+god);
|
||
if(!d){ out.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
|
||
const rows = (d.results || []).slice().sort((a,b)=>Number(b.iznos_eur||0)-Number(a.iznos_eur||0)).slice(0, 25);
|
||
$('#dash-nos-cnt').textContent = rows.length+' / '+(d.count||0)+' · ukupno '+fmtEur(d.total||0);
|
||
out.innerHTML = `<div style="overflow-x:auto"><table>
|
||
<thead><tr><th>#</th><th>Korisnik</th><th>Sport</th><th>Vrsta</th><th class="num">Iznos</th><th>PDF</th></tr></thead>
|
||
<tbody>${rows.map((r,i) => `
|
||
<tr onclick='openPrimateljDetail(${JSON.stringify(r).replace(/'/g,"'")})'>
|
||
<td>${i+1}</td>
|
||
<td><b>${esc(r.korisnik)}</b></td>
|
||
<td>${txt(r.sport)}</td>
|
||
<td>${txt(r.vrsta)}</td>
|
||
<td class="num"><b>${fmtEurFull(r.iznos_eur)}</b></td>
|
||
<td>${r.source_url?'<a href="'+esc(r.source_url)+'" target="_blank" onclick="event.stopPropagation()">📄 PDF</a>':'—'}</td>
|
||
</tr>`).join('')}</tbody>
|
||
</table></div>`;
|
||
}
|
||
|
||
async function openSavezByName(name){
|
||
const list = _cache.savezi || (await api('/savezi?limit=250'))?.rows || [];
|
||
if(!_cache.savezi) _cache.savezi = list;
|
||
const target = list.find(s => s.naziv === name) || list.find(s => (s.naziv||'').toLowerCase() === name.toLowerCase());
|
||
if(target) return openSavez(target.id);
|
||
// Try fuzzy match by first two words
|
||
const prefix = name.split(' ').slice(0,2).join(' ').toLowerCase();
|
||
const fuzzy = list.find(s => (s.naziv||'').toLowerCase().includes(prefix));
|
||
if(fuzzy) return openSavez(fuzzy.id);
|
||
openPanel('Savez', '<div class="empty">Savez <b>'+esc(name)+'</b> nije pronađen u bazi.</div>');
|
||
}
|
||
|
||
async function openPrimateljDetail(r){
|
||
openPanel('Primatelj', '<div class="loading">Učitavanje detalja…</div>');
|
||
// Try to find matching klub by name
|
||
let klub = null;
|
||
if(_cache.klubovi){
|
||
const lower = (r.korisnik||'').toLowerCase();
|
||
klub = _cache.klubovi.find(k => (k.klub||'').toLowerCase() === lower);
|
||
if(!klub) klub = _cache.klubovi.find(k => (k.klub||'').toLowerCase().includes(lower) || lower.includes((k.klub||'').toLowerCase()));
|
||
}
|
||
if(klub){
|
||
return openKlub(klub.id);
|
||
}
|
||
// Fallback panel — show row data + PDF + same-year other primatelji from same source
|
||
const html = `
|
||
<div class="card-h" style="border:0;padding:0;margin-bottom:14px">
|
||
<div>
|
||
<div style="font-size:18px;font-weight:800;color:var(--t0)">${esc(r.korisnik)}</div>
|
||
<div style="font-size:12px;color:var(--t2);margin-top:4px">Primatelj proračuna ${r.godina}</div>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">📋 Detalji isplate</div></div>
|
||
<div class="kv">
|
||
<div class="k">Korisnik</div><div class="v">${esc(r.korisnik)}</div>
|
||
<div class="k">Sport</div><div class="v">${txt(r.sport)}</div>
|
||
<div class="k">Vrsta</div><div class="v">${txt(r.vrsta)}</div>
|
||
<div class="k">Iznos</div><div class="v"><b style="color:var(--pgz-gold);font-size:16px">${fmtEurFull(r.iznos_eur)}</b></div>
|
||
<div class="k">Godina</div><div class="v">${esc(r.godina)}</div>
|
||
<div class="k">Izvor</div><div class="v">${txt(r.izvor)}</div>
|
||
<div class="k">Napomena</div><div class="v">${txt(r.napomena)}</div>
|
||
</div>
|
||
${r.source_url ? '<div style="margin-top:14px;text-align:center"><a href="'+esc(r.source_url)+'" target="_blank" class="btn primary" style="display:inline-block;text-decoration:none">📄 Otvori izvorni PDF dokument</a></div>' : '<div class="empty" style="margin-top:14px">PDF dokument nije dostupan za ovu stavku.</div>'}
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">🔍 Klub nije povezan</div></div>
|
||
<div style="font-size:12px;color:var(--t2);line-height:1.5">
|
||
Korisnik <b>${esc(r.korisnik)}</b> nije automatski povezan s pojedinim klubom u bazi.
|
||
Provjeri klub ručno preko sekcije <a href="javascript:navTo('klubovi')">Klubovi</a> ili pretraži primatelja na izvornom dokumentu.
|
||
</div>
|
||
</div>
|
||
`;
|
||
openPanel('Primatelj · '+r.korisnik, html);
|
||
}
|
||
|
||
function drawProracunChart(trend){
|
||
const ctx = $('#chProracun');
|
||
if(!ctx) return;
|
||
if(_proracunChart){ _proracunChart.destroy(); _proracunChart=null; }
|
||
_proracunChart = new Chart(ctx, {
|
||
type:'bar',
|
||
data:{
|
||
labels: trend.map(x=>x.godina),
|
||
datasets:[{
|
||
label:'EUR',
|
||
data: trend.map(x=>x.ukupno),
|
||
backgroundColor: trend.map(x=> x.godina===2026 ? '#F4C430' : '#004CC4'),
|
||
borderRadius:4
|
||
}]
|
||
},
|
||
options:{
|
||
responsive:true, maintainAspectRatio:false,
|
||
plugins:{
|
||
legend:{display:false},
|
||
tooltip:{callbacks:{label:c => '€'+Number(c.parsed.y).toLocaleString('hr-HR')}}
|
||
},
|
||
scales:{
|
||
x:{ticks:{color:'#8a95b4'}, grid:{display:false}},
|
||
y:{ticks:{color:'#8a95b4', callback:v=>'€'+(v/1000)+'k'}, grid:{color:'#1e2a50'}}
|
||
},
|
||
onClick:(e, els) => {
|
||
if(els && els.length){
|
||
const i = els[0].index;
|
||
openProracunDrill(trend[i].godina, trend[i].ukupno);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
async function openProracunDrill(godina, total){
|
||
openPanel('Proračun '+godina, '<div class="loading">Dohvaćanje primatelja…</div>');
|
||
const d = await api('/v2/potpore/by-year?godina='+godina);
|
||
if(!d){ openPanel('Proračun '+godina, '<div class="empty">Greška pri dohvatu</div>'); return; }
|
||
const rows = d.results || [];
|
||
const grandTotal = d.total || total || 0;
|
||
const html = `
|
||
<div class="kpi-grid" style="grid-template-columns:1fr 1fr">
|
||
<div class="kpi"><div class="kpi-l">Ukupno ${godina}</div><div class="kpi-v">${fmtEur(grandTotal)}</div></div>
|
||
<div class="kpi b"><div class="kpi-l">Primatelja</div><div class="kpi-v">${rows.length}</div></div>
|
||
</div>
|
||
<div style="overflow-x:auto"><table>
|
||
<thead><tr><th>#</th><th>Korisnik</th><th>Sport</th><th class="num">Iznos</th><th>PDF</th></tr></thead>
|
||
<tbody>
|
||
${rows.map((r,i) => `
|
||
<tr class="no-click">
|
||
<td>${i+1}</td>
|
||
<td>${esc(r.korisnik)}</td>
|
||
<td>${txt(r.sport)}</td>
|
||
<td class="num"><b>${fmtEurFull(r.iznos_eur)}</b></td>
|
||
<td>${r.source_url ? '<a href="'+esc(r.source_url)+'" target="_blank">📄</a>' : '—'}</td>
|
||
</tr>`).join('')}
|
||
</tbody>
|
||
</table></div>
|
||
`;
|
||
openPanel('Primatelji proračuna · '+godina, html);
|
||
}
|
||
|
||
//=========== SAVEZI ===========
|
||
async function loadSavezi(){
|
||
const root = $('#pg-savezi');
|
||
if(!_cache.savezi){
|
||
root.innerHTML = '<div class="loading">Učitavanje saveza…</div>';
|
||
const d = await api('/savezi?limit=250');
|
||
if(!d){ root.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
|
||
_cache.savezi = d.rows || [];
|
||
}
|
||
renderSaveziShell();
|
||
applySaveziFilter();
|
||
}
|
||
function renderSaveziShell(){
|
||
const root = $('#pg-savezi');
|
||
root.innerHTML = `
|
||
<div class="toolbar">
|
||
<input type="search" id="sav-q" placeholder="🔍 Pretraži savez…">
|
||
<select id="sav-pgz">
|
||
<option value="">Svi savezi</option>
|
||
<option value="1">Samo PGŽ relevantni</option>
|
||
</select>
|
||
<div class="toggle">
|
||
<button id="sav-card" class="${_state.viewSavezi==='card'?'active':''}" onclick="setSaveziView('card')">Kartice</button>
|
||
<button id="sav-table" class="${_state.viewSavezi==='table'?'active':''}" onclick="setSaveziView('table')">Tablica</button>
|
||
</div>
|
||
<span class="tb-s" id="sav-cnt"></span>
|
||
</div>
|
||
<div id="sav-out"></div>
|
||
`;
|
||
$('#sav-q').addEventListener('input', debounce(applySaveziFilter, 200));
|
||
$('#sav-pgz').addEventListener('change', applySaveziFilter);
|
||
}
|
||
function setSaveziView(v){
|
||
_state.viewSavezi = v;
|
||
$('#sav-card').classList.toggle('active', v==='card');
|
||
$('#sav-table').classList.toggle('active', v==='table');
|
||
applySaveziFilter();
|
||
}
|
||
function applySaveziFilter(){
|
||
const q = (($('#sav-q')?$('#sav-q').value:'') || '').toLowerCase().trim();
|
||
const pgz = $('#sav-pgz') ? $('#sav-pgz').value : '';
|
||
let rows = _cache.savezi || [];
|
||
if(q) rows = rows.filter(s => (s.naziv||'').toLowerCase().includes(q) || (s.sport||'').toLowerCase().includes(q));
|
||
if(pgz==='1') rows = rows.filter(s => s.pgz_relevant);
|
||
if(_sort.savezi) rows = sortRows(rows, _sort.savezi.key, _sort.savezi.dir);
|
||
$('#sav-cnt').textContent = rows.length+' saveza';
|
||
$('#sav-out').innerHTML = _state.viewSavezi==='card' ? renderSaveziGrid(rows) : renderSaveziTable(rows);
|
||
}
|
||
function renderSaveziGrid(rows){
|
||
if(!rows.length) return '<div class="empty">Nema rezultata</div>';
|
||
return '<div class="grid">'+rows.map(s => `
|
||
<div class="entity" onclick="openSavez(${s.id})">
|
||
${s.pgz_relevant?'<div class="et-tag">PGŽ</div>':''}
|
||
<div class="et">${esc(s.naziv)}</div>
|
||
<div class="es">${txt(s.sport,'—')} · ${txt(s.predsjednik,'bez predsjednika')}</div>
|
||
<div class="em">
|
||
<span><b>${fmtNum(s.broj_klubova)}</b> klubova</span>
|
||
<span><b>${fmtNum(s.reg_2024)}</b> reg.</span>
|
||
<span><b>${fmtNum(s.treneri_2024)}</b> trenera</span>
|
||
</div>
|
||
</div>`).join('')+'</div>';
|
||
}
|
||
function renderSaveziTable(rows){
|
||
if(!rows.length) return '<div class="empty">Nema rezultata</div>';
|
||
return `<div class="card" style="padding:0;overflow-x:auto"><table>
|
||
<thead><tr>${sortHeader('savezi','naziv','Naziv','')}${sortHeader('savezi','sport','Sport','')}${sortHeader('savezi','predsjednik','Predsjednik','')}${sortHeader('savezi','email','Email','')}${sortHeader('savezi','broj_klubova','Klubova','num')}${sortHeader('savezi','reg_2024','Reg.','num')}${sortHeader('savezi','pgz_relevant','PGŽ','')}</tr></thead>
|
||
<tbody>${rows.map(s => `
|
||
<tr onclick="openSavez(${s.id})">
|
||
<td><b>${esc(s.naziv)}</b></td>
|
||
<td>${txt(s.sport)}</td>
|
||
<td>${txt(s.predsjednik)}</td>
|
||
<td>${s.email?'<span class="tag b">'+esc(s.email)+'</span>':'—'}</td>
|
||
<td class="num">${fmtNum(s.broj_klubova)}</td>
|
||
<td class="num">${fmtNum(s.reg_2024)}</td>
|
||
<td>${s.pgz_relevant?'<span class="tag gd">PGŽ</span>':'—'}</td>
|
||
</tr>`).join('')}</tbody>
|
||
</table></div>`;
|
||
}
|
||
|
||
async function openSavez(id){
|
||
openPanel('Savez', '<div class="loading">Učitavanje saveza…</div>');
|
||
const s = await api('/savezi/'+id);
|
||
if(!s || s.detail){
|
||
openPanel('Savez', '<div class="empty">Savez nije pronađen</div>');
|
||
return;
|
||
}
|
||
const ksearch = (s.naziv||'').split(' ').slice(0,2).join(' ');
|
||
const kr = await api('/klubovi?savez='+encodeURIComponent(ksearch)+'&limit=200');
|
||
const klubovi = (kr && kr.rows) ? kr.rows.filter(k => (k.savez||'').toLowerCase().includes(ksearch.toLowerCase())) : [];
|
||
|
||
const html = `
|
||
<div class="card-h" style="border:0;padding:0;margin-bottom:14px">
|
||
<div>
|
||
<div style="font-size:18px;font-weight:800;color:var(--t0)">${esc(s.naziv)}</div>
|
||
<div style="font-size:12px;color:var(--t2);margin-top:4px">${txt(s.sport,'—')} · ${s.region||'PGŽ'}</div>
|
||
</div>
|
||
</div>
|
||
<div class="kpi-grid" style="grid-template-columns:repeat(3,1fr);margin-bottom:14px">
|
||
<div class="kpi"><div class="kpi-l">Klubova</div><div class="kpi-v">${klubovi.length}</div></div>
|
||
<div class="kpi b"><div class="kpi-l">Godina osnutka</div><div class="kpi-v" style="font-size:18px">${txt(s.godina_osnutka)}</div></div>
|
||
<div class="kpi g"><div class="kpi-l">Status</div><div class="kpi-v" style="font-size:14px">${s.aktivan?'AKTIVAN':'NEAKTIVAN'}</div></div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">📋 Osnovne informacije</div></div>
|
||
<div class="kv">
|
||
<div class="k">OIB</div><div class="v">${txt(s.oib)}</div>
|
||
<div class="k">Adresa</div><div class="v">${txt(s.adresa)}</div>
|
||
<div class="k">Predsjednik</div><div class="v">${txt(s.predsjednik)}</div>
|
||
<div class="k">Tajnik</div><div class="v">${txt(s.tajnik)}</div>
|
||
<div class="k">Email</div><div class="v">${s.email?'<a href="mailto:'+esc(s.email)+'">'+esc(s.email)+'</a>':'—'}</div>
|
||
<div class="k">Telefon</div><div class="v">${txt(s.telefon)}</div>
|
||
<div class="k">Web</div><div class="v">${s.web?'<a href="'+esc(s.web)+'" target="_blank">'+esc(s.web)+'</a>':'—'}</div>
|
||
<div class="k">IBAN</div><div class="v">${txt(s.iban)}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">⬡ Klubovi članice (${klubovi.length})</div></div>
|
||
${klubovi.length ? `<div style="overflow-x:auto;max-height:400px;overflow-y:auto"><table>
|
||
<thead><tr><th>Klub</th><th>Razina</th><th>Grad</th></tr></thead>
|
||
<tbody>${klubovi.slice(0,100).map(k => `
|
||
<tr onclick="closePanel();setTimeout(()=>openKlub(${k.id}),250)">
|
||
<td>${esc(k.klub||k.sport||'(bez naziva)')}${k.nositelj_kvalitete?' <span class="tag gd">N.K.</span>':''}</td>
|
||
<td>${txt(k.razina,'')}</td>
|
||
<td>${txt(k.grad)}</td>
|
||
</tr>`).join('')}
|
||
</tbody>
|
||
</table></div>` : '<div class="empty">Nema podataka o klubovima</div>'}
|
||
</div>
|
||
|
||
${enrichBlock('savez', s.id)}
|
||
`;
|
||
openPanel('Savez · '+s.naziv, html);
|
||
}
|
||
|
||
//=========== KLUBOVI ===========
|
||
async function loadKlubovi(){
|
||
const root = $('#pg-klubovi');
|
||
if(!_cache.klubovi){
|
||
root.innerHTML = '<div class="loading">Učitavanje klubova…</div>';
|
||
const d = await api('/klubovi?limit=500');
|
||
if(!d){ root.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
|
||
_cache.klubovi = d.rows || [];
|
||
}
|
||
renderKluboviShell();
|
||
applyKluboviFilter();
|
||
}
|
||
function renderKluboviShell(){
|
||
const root = $('#pg-klubovi');
|
||
const sports = Array.from(new Set((_cache.klubovi||[]).map(k=>k.sport).filter(Boolean))).sort().slice(0,80);
|
||
const grads = Array.from(new Set((_cache.klubovi||[]).map(k=>k.grad).filter(Boolean))).sort();
|
||
root.innerHTML = `
|
||
<div class="toolbar">
|
||
<input type="search" id="kl-q" placeholder="🔍 Pretraži klub…">
|
||
<select id="kl-sport"><option value="">Svi sportovi</option>${sports.map(s=>'<option value="'+esc(s)+'">'+esc(s)+'</option>').join('')}</select>
|
||
<select id="kl-grad"><option value="">Svi gradovi</option>${grads.map(g=>'<option value="'+esc(g)+'">'+esc(g)+'</option>').join('')}</select>
|
||
<label><input type="checkbox" id="kl-nk"> Nositelj kvalitete</label>
|
||
<div class="toggle">
|
||
<button id="kl-card" class="${_state.viewKlubovi==='card'?'active':''}" onclick="setKluboviView('card')">Kartice</button>
|
||
<button id="kl-table" class="${_state.viewKlubovi==='table'?'active':''}" onclick="setKluboviView('table')">Tablica</button>
|
||
</div>
|
||
<span class="tb-s" id="kl-cnt"></span>
|
||
</div>
|
||
<div id="kl-out"></div>
|
||
`;
|
||
$('#kl-q').addEventListener('input', debounce(applyKluboviFilter, 200));
|
||
$('#kl-sport').addEventListener('change', applyKluboviFilter);
|
||
$('#kl-grad').addEventListener('change', applyKluboviFilter);
|
||
$('#kl-nk').addEventListener('change', applyKluboviFilter);
|
||
}
|
||
function setKluboviView(v){
|
||
_state.viewKlubovi = v;
|
||
$('#kl-card').classList.toggle('active', v==='card');
|
||
$('#kl-table').classList.toggle('active', v==='table');
|
||
applyKluboviFilter();
|
||
}
|
||
function applyKluboviFilter(){
|
||
const q = (($('#kl-q')?$('#kl-q').value:'') || '').toLowerCase().trim();
|
||
const sport = $('#kl-sport') ? $('#kl-sport').value : '';
|
||
const grad = $('#kl-grad') ? $('#kl-grad').value : '';
|
||
const nk = $('#kl-nk') ? $('#kl-nk').checked : false;
|
||
let rows = _cache.klubovi || [];
|
||
if(q) rows = rows.filter(k => (k.klub||'').toLowerCase().includes(q) || (k.sport||'').toLowerCase().includes(q));
|
||
if(sport) rows = rows.filter(k => k.sport===sport);
|
||
if(grad) rows = rows.filter(k => k.grad===grad);
|
||
if(nk) rows = rows.filter(k => k.nositelj_kvalitete);
|
||
if(_sort.klubovi) rows = sortRows(rows, _sort.klubovi.key, _sort.klubovi.dir);
|
||
$('#kl-cnt').textContent = rows.length+' klubova';
|
||
const top = rows.slice(0, 300);
|
||
$('#kl-out').innerHTML = _state.viewKlubovi==='card' ? renderKluboviGrid(top) : renderKluboviTable(top);
|
||
if(rows.length>300){
|
||
$('#kl-out').insertAdjacentHTML('beforeend', '<div class="empty">… i još '+(rows.length-300)+' klubova. Suzite filtre.</div>');
|
||
}
|
||
}
|
||
function renderKluboviGrid(rows){
|
||
if(!rows.length) return '<div class="empty">Nema rezultata</div>';
|
||
return '<div class="grid-club">'+rows.map(k => `
|
||
<div class="entity" onclick="openKlub(${k.id})">
|
||
${k.nositelj_kvalitete?'<div class="et-tag">N.K.</div>':''}
|
||
<div class="et">${esc(k.klub||k.sport||'(bez naziva)')}</div>
|
||
<div class="es">${txt(k.razina,'')} · ${txt(k.grad,'—')}</div>
|
||
<div class="em">
|
||
<span><b>${fmtNum(k.registriranih)}</b> reg.</span>
|
||
<span><b>${fmtNum(k.trenera)}</b> trenera</span>
|
||
<span><b>${fmtNum(k.reprezentativaca)}</b> repr.</span>
|
||
</div>
|
||
</div>`).join('')+'</div>';
|
||
}
|
||
function renderKluboviTable(rows){
|
||
if(!rows.length) return '<div class="empty">Nema rezultata</div>';
|
||
return `<div class="card" style="padding:0;overflow-x:auto"><table>
|
||
<thead><tr>${sortHeader('klubovi','klub','Klub','')}${sortHeader('klubovi','sport','Sport','')}${sortHeader('klubovi','razina','Razina','')}${sortHeader('klubovi','grad','Grad','')}${sortHeader('klubovi','registriranih','Reg.','num')}${sortHeader('klubovi','trenera','Trenera','num')}${sortHeader('klubovi','nositelj_kvalitete','Status','')}</tr></thead>
|
||
<tbody>${rows.map(k => `
|
||
<tr onclick="openKlub(${k.id})">
|
||
<td><b>${esc(k.klub||k.sport||'(bez naziva)')}</b></td>
|
||
<td>${txt(k.sport)}</td>
|
||
<td>${txt(k.razina)}</td>
|
||
<td>${txt(k.grad)}</td>
|
||
<td class="num">${fmtNum(k.registriranih)}</td>
|
||
<td class="num">${fmtNum(k.trenera)}</td>
|
||
<td>${k.nositelj_kvalitete?'<span class="tag gd">N.K.</span>':''}${k.aktivan?'<span class="tag gr">AKT</span>':'<span class="tag rd">NK</span>'}</td>
|
||
</tr>`).join('')}</tbody>
|
||
</table></div>`;
|
||
}
|
||
|
||
async function openKlub(id){
|
||
openPanel('Klub', '<div class="loading">Učitavanje kluba…</div>');
|
||
const k = await api('/klubovi/'+id);
|
||
if(!k || k.detail){
|
||
openPanel('Klub', '<div class="empty">Klub nije pronađen</div>');
|
||
return;
|
||
}
|
||
const stats = k.stats || {};
|
||
const clanovi = k.clanovi || [];
|
||
const potpore = k.potpore || [];
|
||
let scoreCount = 0;
|
||
if(k.oib) scoreCount++;
|
||
if(k.predsjednik) scoreCount++;
|
||
if(k.tajnik) scoreCount++;
|
||
if(k.email) scoreCount++;
|
||
if(k.web || k.web_stranica) scoreCount++;
|
||
if(k.sjediste || k.adresa) scoreCount++;
|
||
if(k.ciljevi) scoreCount++;
|
||
if(k.opis_djelatnosti) scoreCount++;
|
||
const scoreClass = scoreCount>=6?'high':(scoreCount>=4?'mid':'low');
|
||
|
||
const html = `
|
||
<div class="card-h" style="border:0;padding:0;margin-bottom:14px">
|
||
<div>
|
||
<div style="font-size:18px;font-weight:800;color:var(--t0)">${esc(k.naziv||'(bez naziva)')}</div>
|
||
<div style="font-size:12px;color:var(--t2);margin-top:4px">${txt(k.sport,'—')} · ${txt(k.razina,'')} · ${txt(k.grad,'')}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="kpi-grid" style="grid-template-columns:repeat(4,1fr);margin-bottom:14px">
|
||
<div class="kpi"><div class="kpi-l">Sportaša</div><div class="kpi-v">${fmtNum(stats.broj_clanova||clanovi.length)}</div></div>
|
||
<div class="kpi b"><div class="kpi-l">Registriranih</div><div class="kpi-v">${fmtNum(stats.broj_registriranih)}</div></div>
|
||
<div class="kpi g"><div class="kpi-l">Trenera</div><div class="kpi-v">${fmtNum(stats.broj_trenera)}</div></div>
|
||
<div class="kpi"><div class="kpi-l">Score baze</div><div class="kpi-v" style="font-size:18px"><span class="score ${scoreClass}">${scoreCount}/8</span></div></div>
|
||
</div>
|
||
|
||
<div class="tabs">
|
||
<div class="tab active" onclick="switchKlubTab(this,'k-info')">Info</div>
|
||
<div class="tab" onclick="switchKlubTab(this,'k-clan')">Sportaši (${clanovi.length})</div>
|
||
<div class="tab" onclick="switchKlubTab(this,'k-pot')">Potpore (${potpore.length})</div>
|
||
</div>
|
||
|
||
<div id="k-info" class="ktab">
|
||
<div class="kv">
|
||
<div class="k">Naziv</div><div class="v">${esc(k.naziv||'')}</div>
|
||
<div class="k">OIB</div><div class="v">${txt(k.oib)}</div>
|
||
<div class="k">Sport</div><div class="v">${txt(k.sport)}</div>
|
||
<div class="k">Razina</div><div class="v">${txt(k.razina)}</div>
|
||
<div class="k">Savez</div><div class="v">${txt(k.savez_naziv)}</div>
|
||
<div class="k">Predsjednik</div><div class="v">${txt(k.predsjednik)}</div>
|
||
<div class="k">Tajnik</div><div class="v">${txt(k.tajnik)}</div>
|
||
<div class="k">Trener</div><div class="v">${txt(k.trener_glavni)}</div>
|
||
<div class="k">Sjedište</div><div class="v">${txt(k.sjediste||k.adresa)}</div>
|
||
<div class="k">Grad</div><div class="v">${txt(k.grad)}</div>
|
||
<div class="k">Email</div><div class="v">${k.email?'<a href="mailto:'+esc(k.email)+'">'+esc(k.email)+'</a>':'—'}</div>
|
||
<div class="k">Telefon</div><div class="v">${txt(k.telefon)}</div>
|
||
<div class="k">Web</div><div class="v">${(k.web||k.web_stranica)?'<a href="'+esc(k.web||k.web_stranica)+'" target="_blank">'+esc(k.web||k.web_stranica)+'</a>':'—'}</div>
|
||
<div class="k">Osnovan</div><div class="v">${txt(k.godina_osnutka)}</div>
|
||
<div class="k">Nositelj kvalitete</div><div class="v">${k.nositelj_kvalitete?'<span class="tag gd">DA</span>':'<span class="tag">NE</span>'}</div>
|
||
</div>
|
||
${k.napomena ? '<div class="card" style="margin-top:14px"><div class="card-t" style="margin-bottom:6px">Napomena</div><div style="font-size:12px;color:var(--t1);line-height:1.5">'+esc(k.napomena)+'</div></div>' : ''}
|
||
</div>
|
||
|
||
<div id="k-clan" class="ktab" style="display:none">
|
||
${clanovi.length ? `<div style="overflow-x:auto;max-height:500px;overflow-y:auto"><table>
|
||
<thead><tr><th>Sportaš</th><th>Spol</th><th>Pozicija</th><th>Tagovi</th></tr></thead>
|
||
<tbody>${clanovi.map(c => `
|
||
<tr onclick="closePanel();setTimeout(()=>openSportas(${c.id}),250)">
|
||
<td><b>${esc(c.ime||'')} ${esc(c.prezime||'')}</b></td>
|
||
<td>${txt(c.spol)}</td>
|
||
<td>${txt(c.pozicija)}</td>
|
||
<td>${c.reprezentativac?'<span class="tag gd">REPR</span>':''}${c.kategoriziran?'<span class="tag b">KAT</span>':''}${c.stipendiran?'<span class="tag gr">STIP</span>':''}</td>
|
||
</tr>`).join('')}
|
||
</tbody>
|
||
</table></div>` : '<div class="empty">Nema podataka o sportašima</div>'}
|
||
</div>
|
||
|
||
<div id="k-pot" class="ktab" style="display:none">
|
||
${potpore.length ? `<div style="overflow-x:auto"><table>
|
||
<thead><tr><th>Godina</th><th>Naziv</th><th class="num">Iznos</th></tr></thead>
|
||
<tbody>${potpore.map(p => `
|
||
<tr class="no-click">
|
||
<td>${esc(p.godina)}</td>
|
||
<td>${esc(p.naziv_kluba||'')}</td>
|
||
<td class="num"><b>${fmtEurFull(p.iznos)}</b></td>
|
||
</tr>`).join('')}
|
||
</tbody>
|
||
</table></div>` : '<div class="empty">Nema zabilježenih potpora</div>'}
|
||
</div>
|
||
|
||
${enrichBlock('klub', k.id)}
|
||
`;
|
||
openPanel('Klub · '+(k.naziv||''), html);
|
||
}
|
||
function switchKlubTab(el, tabId){
|
||
const parent = el.parentElement;
|
||
parent.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
|
||
el.classList.add('active');
|
||
parent.parentElement.querySelectorAll('.ktab').forEach(t=>t.style.display='none');
|
||
const target = document.getElementById(tabId);
|
||
if(target) target.style.display='block';
|
||
}
|
||
|
||
//=========== SPORTAŠI ===========
|
||
async function loadSportasi(){
|
||
const root = $('#pg-sportasi');
|
||
if(!_cache.clanovi){
|
||
root.innerHTML = '<div class="loading">Učitavanje sportaša…</div>';
|
||
const d = await api('/clanovi-full?limit=500');
|
||
if(!d){ root.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
|
||
_cache.clanovi = d.rows || [];
|
||
}
|
||
renderSportasiShell();
|
||
applySportasiFilter();
|
||
}
|
||
function renderSportasiShell(){
|
||
const root = $('#pg-sportasi');
|
||
root.innerHTML = `
|
||
<div class="toolbar">
|
||
<input type="search" id="sp-q" placeholder="🔍 Ime ili prezime…">
|
||
<select id="sp-hoo">
|
||
<option value="">Sve HOO kategorije</option>
|
||
<option value="1">I. kategorija</option>
|
||
<option value="2">II. kategorija</option>
|
||
<option value="3">III. kategorija</option>
|
||
<option value="4">IV. kategorija</option>
|
||
<option value="5">V. kategorija</option>
|
||
</select>
|
||
<label><input type="checkbox" id="sp-rep"> Samo reprezentativci</label>
|
||
<label><input type="checkbox" id="sp-foto"> Samo s fotografijom</label>
|
||
<div class="toggle">
|
||
<button id="sp-card" class="${_state.viewSportasi==='card'?'active':''}" onclick="setSportasiView('card')">Kartice</button>
|
||
<button id="sp-table" class="${_state.viewSportasi==='table'?'active':''}" onclick="setSportasiView('table')">Tablica</button>
|
||
</div>
|
||
<span class="tb-s" id="sp-cnt"></span>
|
||
<button class="btn primary" onclick="openSportas(449)">⭐ Test: Josip Zec</button>
|
||
</div>
|
||
<div id="sp-out"></div>
|
||
`;
|
||
$('#sp-q').addEventListener('input', debounce(applySportasiFilter, 200));
|
||
$('#sp-hoo').addEventListener('change', applySportasiFilter);
|
||
$('#sp-rep').addEventListener('change', applySportasiFilter);
|
||
$('#sp-foto').addEventListener('change', applySportasiFilter);
|
||
}
|
||
function openOIB(oib){
|
||
const cleanOib = String(oib||'').replace(/[^0-9]/g,'');
|
||
const url = 'https://sudreg.pravosudje.hr/registar/oc/index.html#osnovniPodaci?o='+encodeURIComponent(cleanOib);
|
||
// Open external — sudreg supports OIB lookup
|
||
window.open(url, '_blank', 'noopener');
|
||
}
|
||
function filterKluboviByCity(grad){
|
||
closePanel();
|
||
navTo('klubovi');
|
||
setTimeout(() => {
|
||
const sel = $('#kl-grad');
|
||
if(sel){ sel.value = grad; applyKluboviFilter(); }
|
||
}, 100);
|
||
}
|
||
function filterKluboviBySport(sport){
|
||
closePanel();
|
||
navTo('klubovi');
|
||
setTimeout(() => {
|
||
const sel = $('#kl-sport');
|
||
if(sel){ sel.value = sport; applyKluboviFilter(); }
|
||
}, 100);
|
||
}
|
||
function filterObjektiByCity(grad){
|
||
closePanel();
|
||
navTo('objekti');
|
||
setTimeout(() => {
|
||
const sel = $('#ob-grad');
|
||
if(sel){ sel.value = grad; applyObjektiFilter(); }
|
||
}, 100);
|
||
}
|
||
function filterSportasiBy(field, value){
|
||
closePanel();
|
||
navTo('sportasi');
|
||
setTimeout(() => {
|
||
if(field === 'sport'){
|
||
// Use search box since there's no sport dropdown
|
||
const q = $('#sp-q'); if(q){ q.value = value; }
|
||
} else if(field === 'mjesto_rodjenja' || field === 'grad'){
|
||
const q = $('#sp-q'); if(q){ q.value = value; }
|
||
} else if(field === 'reprezentativac'){
|
||
const cb = $('#sp-rep'); if(cb){ cb.checked = !!value; }
|
||
} else if(field === 'hoo'){
|
||
const sel = $('#sp-hoo'); if(sel){ sel.value = String(value); }
|
||
} else if(field === 'aktivan'){
|
||
// Add to extra-filters slot if exists; else search by status string
|
||
_state.spExtraAktivan = value ? 'true' : 'false';
|
||
} else if(field === 'stipendiran'){
|
||
_state.spExtraStipendiran = !!value;
|
||
}
|
||
applySportasiFilter();
|
||
}, 100);
|
||
}
|
||
function filterSportasiByYear(year){
|
||
closePanel();
|
||
navTo('sportasi');
|
||
setTimeout(() => {
|
||
_state.spYear = String(year);
|
||
applySportasiFilter();
|
||
}, 100);
|
||
}
|
||
function clearSportasiExtras(){
|
||
_state.spExtraAktivan = '';
|
||
_state.spExtraStipendiran = false;
|
||
_state.spYear = '';
|
||
}
|
||
|
||
function setSportasiView(v){
|
||
_state.viewSportasi = v;
|
||
$('#sp-card').classList.toggle('active', v==='card');
|
||
$('#sp-table').classList.toggle('active', v==='table');
|
||
applySportasiFilter();
|
||
}
|
||
function applySportasiFilter(){
|
||
const q = (($('#sp-q')?$('#sp-q').value:'') || '').toLowerCase().trim();
|
||
const hoo = $('#sp-hoo') ? $('#sp-hoo').value : '';
|
||
const rep = $('#sp-rep') ? $('#sp-rep').checked : false;
|
||
const foto = $('#sp-foto') ? $('#sp-foto').checked : false;
|
||
let rows = _cache.clanovi || [];
|
||
if(q) rows = rows.filter(c => ((c.ime||'')+' '+(c.prezime||'')).toLowerCase().includes(q));
|
||
if(rep) rows = rows.filter(c => c.reprezentativac);
|
||
if(foto) rows = rows.filter(c => c.slika_url);
|
||
if(hoo) rows = rows.filter(c => String(c.hoo_kategorija||c.kategorija_hoo||'')===hoo);
|
||
if(_state.spYear){
|
||
rows = rows.filter(c => String(c.datum_rodenja||c.datum_rodjenja||'').slice(0,4) === _state.spYear);
|
||
}
|
||
if(_state.spExtraAktivan==='true') rows = rows.filter(c => c.aktivan);
|
||
if(_state.spExtraAktivan==='false') rows = rows.filter(c => !c.aktivan);
|
||
if(_state.spExtraStipendiran) rows = rows.filter(c => c.stipendiran);
|
||
if(_sort.sportasi) rows = sortRows(rows, _sort.sportasi.key, _sort.sportasi.dir);
|
||
$('#sp-cnt').textContent = rows.length+' sportaša';
|
||
const top = rows.slice(0, 300);
|
||
$('#sp-out').innerHTML = _state.viewSportasi==='card' ? renderSportasiGrid(top) : renderSportasiTable(top);
|
||
if(rows.length>300){
|
||
$('#sp-out').insertAdjacentHTML('beforeend', '<div class="empty">… i još '+(rows.length-300)+' sportaša. Suzite filtre.</div>');
|
||
}
|
||
}
|
||
function renderSportasiGrid(rows){
|
||
if(!rows.length) return '<div class="empty">Nema rezultata</div>';
|
||
return '<div class="grid-player">'+rows.map(c => buildPlayerCard(c)).join('')+'</div>';
|
||
}
|
||
function buildPlayerCard(c){
|
||
const initials = (((c.ime||'?')[0]||'?')+((c.prezime||'?')[0]||'?')).toUpperCase();
|
||
const photo = c.slika_url ? '<img src="'+esc(c.slika_url)+'" alt="" onerror="this.style.display=\'none\';if(this.parentElement)this.parentElement.innerHTML=\'<div class=\\\'no\\\'>'+initials+'</div>\'">' : '<div class="no">'+initials+'</div>';
|
||
const hooCat = c.hoo_kategorija || c.kategorija_hoo;
|
||
return `
|
||
<div class="player-card" onclick="openSportas(${c.id})">
|
||
<div class="ph">${photo}</div>
|
||
<div class="pb">
|
||
<div class="pn">${esc(c.ime||'')} ${esc(c.prezime||'')}</div>
|
||
<div class="pp">${txt(c.sport,'—')} · ${txt(c.pozicija,'')}</div>
|
||
<div class="pk">${txt(c.klub_naziv_godisnjak,'')}</div>
|
||
<div class="badges">
|
||
${c.reprezentativac?'<span class="badge repr">REPR</span>':''}
|
||
${hooCat?'<span class="badge hoo">HOO '+esc(hooCat)+'</span>':''}
|
||
${c.stipendiran?'<span class="badge">STIP</span>':''}
|
||
${c.broj_dresa?'<span class="badge">#'+esc(c.broj_dresa)+'</span>':''}
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
function renderSportasiTable(rows){
|
||
if(!rows.length) return '<div class="empty">Nema rezultata</div>';
|
||
return `<div class="card" style="padding:0;overflow-x:auto"><table>
|
||
<thead><tr>${sortHeader('sportasi','prezime','Prezime','')}${sortHeader('sportasi','ime','Ime','')}${sortHeader('sportasi','sport','Sport','')}${sortHeader('sportasi','pozicija','Pozicija','')}${sortHeader('sportasi','hoo_kategorija','HOO','')}${sortHeader('sportasi','reprezentativac','Repr.','')}${sortHeader('sportasi','slika_url','Foto','')}</tr></thead>
|
||
<tbody>${rows.map(c => `
|
||
<tr onclick="openSportas(${c.id})">
|
||
<td><b>${esc(c.prezime||'')}</b></td>
|
||
<td>${esc(c.ime||'')}</td>
|
||
<td>${txt(c.sport)}</td>
|
||
<td>${txt(c.pozicija)}</td>
|
||
<td>${(c.hoo_kategorija||c.kategorija_hoo)?'<span class="tag b">'+esc(c.hoo_kategorija||c.kategorija_hoo)+'</span>':'—'}</td>
|
||
<td>${c.reprezentativac?'<span class="tag gd">DA</span>':'—'}</td>
|
||
<td>${c.slika_url?'📸':'—'}</td>
|
||
</tr>`).join('')}</tbody>
|
||
</table></div>`;
|
||
}
|
||
|
||
async function openSportas(id){
|
||
openPanel('Sportaš', '<div class="loading">Učitavanje profila…</div>');
|
||
const d = await api('/sportas/'+id+'/profil');
|
||
if(!d || d.detail){
|
||
openPanel('Sportaš', '<div class="empty">Sportaš nije pronađen</div>');
|
||
return;
|
||
}
|
||
const stats = d.stats || {};
|
||
const sezone = d.clan_sezona || [];
|
||
const utakmice = d.utakmice || [];
|
||
const nagrade = d.nagrade || [];
|
||
const godisnjaci = d.godisnjak_godine || d.godisnjaci || [];
|
||
const initials = (((d.ime||'?')[0]||'?')+((d.prezime||'?')[0]||'?')).toUpperCase();
|
||
const photo = d.slika_url ? '<img src="'+esc(d.slika_url)+'" alt="" onerror="this.style.display=\'none\';if(this.parentElement)this.parentElement.innerHTML=\'<div class=\\\'no\\\'>'+initials+'</div>\'">' : '<div class="no">'+initials+'</div>';
|
||
const dob = d.datum_rodjenja || d.datum_rodenja;
|
||
const hooCat = d.hoo_kategorija || d.kategorija_hoo;
|
||
|
||
const html = `
|
||
<div class="pp-hdr">
|
||
<div class="pp-foto">${photo}</div>
|
||
<div class="pp-info">
|
||
<div class="pp-name">${esc(d.ime||'')} ${esc(d.prezime||'')}</div>
|
||
<div class="pp-meta">
|
||
${d.sport?'<a class="link-chip" onclick="filterSportasiBy("sport","'+esc(d.sport)+'")">'+esc(d.sport)+'</a>':'—'} ·
|
||
${txt(d.pozicija,'')} ·
|
||
${d.klub_id ? '<a class="link-chip" onclick="closePanel();setTimeout(()=>openKlub('+d.klub_id+'),250)"><b>'+esc(d.klub_naziv_full||d.klub_naziv_godisnjak||'—')+'</b></a>' : '<b>'+esc(d.klub_naziv_full||d.klub_naziv_godisnjak||'—')+'</b>'}
|
||
</div>
|
||
<div class="pp-meta">
|
||
${dob ? '<a class="link-chip" onclick="filterSportasiByYear("'+esc((dob||'').slice(0,4))+'")">📅 '+fmtDate(dob)+'</a>' : '📅 —'}
|
||
${(d.mjesto_rodjenja||d.mjesto_rodenja)?' · <a class="link-chip" onclick="filterSportasiBy("mjesto_rodjenja","'+esc(d.mjesto_rodjenja||d.mjesto_rodenja)+'")">'+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("reprezentativac",true)">REPR</a>':''}
|
||
${hooCat?'<a class="tag b" onclick="filterSportasiBy("hoo","'+esc(hooCat)+'")">HOO '+esc(hooCat)+'</a>':''}
|
||
${d.broj_dresa?'<span class="tag">#'+esc(d.broj_dresa)+'</span>':''}
|
||
${d.stipendiran?'<a class="tag am" onclick="filterSportasiBy("stipendiran",true)">STIP</a>':''}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pp-stats">
|
||
<div class="pp-stat"><div class="v">${fmtNum(stats.ukupno_nastupa||0)}</div><div class="l">Nastupi</div></div>
|
||
<div class="pp-stat"><div class="v">${fmtNum(stats.ukupno_pogodaka||0)}</div><div class="l">Golovi</div></div>
|
||
<div class="pp-stat"><div class="v">${fmtNum(stats.ukupno_asistencija||0)}</div><div class="l">Asist.</div></div>
|
||
<div class="pp-stat"><div class="v" style="color:var(--amber)">${fmtNum(stats.ukupno_zutih||0)}</div><div class="l">Žuti</div></div>
|
||
<div class="pp-stat"><div class="v" style="color:var(--red)">${fmtNum(stats.ukupno_crvenih||0)}</div><div class="l">Crveni</div></div>
|
||
<div class="pp-stat"><div class="v">${fmtNum(stats.sezone_aktivne||sezone.length)}</div><div class="l">Sezona</div></div>
|
||
</div>
|
||
|
||
<div class="tabs">
|
||
<div class="tab active" onclick="switchPlayerTab(this,'p-sez')">Sezone (${sezone.length})</div>
|
||
<div class="tab" onclick="switchPlayerTab(this,'p-utak')">Utakmice (${utakmice.length})</div>
|
||
<div class="tab" onclick="switchPlayerTab(this,'p-bio')">Bio</div>
|
||
<div class="tab" onclick="switchPlayerTab(this,'p-god')">Godišnjaci (${godisnjaci.length})</div>
|
||
<div class="tab" onclick="switchPlayerTab(this,'p-nag')">Nagrade (${nagrade.length})</div>
|
||
</div>
|
||
|
||
<div id="p-sez" class="ptab">
|
||
${sezone.length ? `<div style="overflow-x:auto"><table>
|
||
<thead><tr><th>Sezona</th><th>Natjecanje</th><th>Klub</th><th class="num">Nas.</th><th class="num">Gol.</th><th class="num">Asis.</th><th class="num">Žuti</th><th class="num">Crv.</th><th></th></tr></thead>
|
||
<tbody>${sezone.map(s => `
|
||
<tr class="no-click">
|
||
<td><b>${esc(s.sezona||'')}</b></td>
|
||
<td>${esc(s.natjecanje||'')}</td>
|
||
<td>${esc(s.klub_naziv||'')}</td>
|
||
<td class="num">${fmtNum(s.nastupi)}</td>
|
||
<td class="num"><b style="color:var(--pgz-gold)">${fmtNum(s.pogoci)}</b></td>
|
||
<td class="num">${fmtNum(s.asistencije)}</td>
|
||
<td class="num">${fmtNum(s.zuti_kartoni)}</td>
|
||
<td class="num">${fmtNum(s.crveni_kartoni)}</td>
|
||
<td>${s.natjecanje_url?'<a href="'+esc(s.natjecanje_url)+'" target="_blank">↗</a>':''}</td>
|
||
</tr>`).join('')}
|
||
</tbody>
|
||
</table></div>` : '<div class="empty">Nema sezonskih podataka</div>'}
|
||
</div>
|
||
|
||
<div id="p-utak" class="ptab" style="display:none">
|
||
${utakmice.length ? `<div style="overflow-x:auto;max-height:500px;overflow-y:auto"><table>
|
||
<thead><tr><th>Datum</th><th>Natjecanje</th><th colspan="3">Susret</th><th class="num">Gol.</th><th class="num">Min.</th><th></th></tr></thead>
|
||
<tbody>${utakmice.map(u => `
|
||
<tr class="no-click">
|
||
<td>${fmtDate(u.datum)}</td>
|
||
<td>${esc(u.natjecanje||'')}</td>
|
||
<td>${u.klub_dom_logo?'<img src="'+esc(u.klub_dom_logo)+'" class="utlogo" onerror="this.style.display=\'none\'">':''}${esc(u.klub_dom||'')}</td>
|
||
<td><b>${esc(u.rezultat||'-')}</b></td>
|
||
<td>${u.klub_gost_logo?'<img src="'+esc(u.klub_gost_logo)+'" class="utlogo" onerror="this.style.display=\'none\'">':''}${esc(u.klub_gost||'')}</td>
|
||
<td class="num"><b style="color:var(--pgz-gold)">${fmtNum(u.pogodaka)}</b></td>
|
||
<td class="num">${fmtNum(u.minute)}</td>
|
||
<td>${u.source_url?'<a href="'+esc(u.source_url)+'" target="_blank">↗</a>':''}</td>
|
||
</tr>`).join('')}
|
||
</tbody>
|
||
</table></div>` : '<div class="empty">Nema podataka o utakmicama</div>'}
|
||
</div>
|
||
|
||
<div id="p-bio" class="ptab" style="display:none">
|
||
<div class="kv">
|
||
<div class="k">OIB</div><div class="v">${d.oib?'<a class="link-chip" onclick="openOIB("'+esc(d.oib)+'")">'+esc(d.oib)+'</a>':'—'}</div>
|
||
<div class="k">Datum rođenja</div><div class="v">${fmtDate(dob)}</div>
|
||
<div class="k">Mjesto rođenja</div><div class="v">${txt(d.mjesto_rodjenja||d.mjesto_rodenja)}</div>
|
||
<div class="k">Spol</div><div class="v">${txt(d.spol)}</div>
|
||
<div class="k">Visina</div><div class="v">${d.visina_cm?d.visina_cm+' cm':'—'}</div>
|
||
<div class="k">Težina</div><div class="v">${d.tezina_kg?d.tezina_kg+' kg':'—'}</div>
|
||
<div class="k">Dom. noga</div><div class="v">${txt(d.dominantna_noga)}</div>
|
||
<div class="k">Status</div><div class="v">${d.aktivan?'AKTIVAN':'NEAKTIVAN'}</div>
|
||
<div class="k">Datum pristupa</div><div class="v">${fmtDate(d.datum_pristupa)}</div>
|
||
<div class="k">Email</div><div class="v">${d.email?'<a href="mailto:'+esc(d.email)+'">'+esc(d.email)+'</a>':'—'}</div>
|
||
<div class="k">Telefon</div><div class="v">${txt(d.telefon)}</div>
|
||
<div class="k">Profil</div><div class="v">${(d.profile_url||d.scrape_url)?'<a href="'+esc(d.profile_url||d.scrape_url)+'" target="_blank">↗ vanjski profil</a>':'—'}</div>
|
||
</div>
|
||
${d.biografija ? '<div class="card" style="margin-top:14px"><div class="card-t">Biografija</div><div style="font-size:12px;line-height:1.5;color:var(--t1);margin-top:6px">'+esc(d.biografija)+'</div></div>' : ''}
|
||
</div>
|
||
|
||
<div id="p-god" class="ptab" style="display:none">
|
||
${godisnjaci.length ? `<div class="kv">
|
||
<div class="k">Prvi godišnjak</div><div class="v">${esc(d.godisnjak_prvi||godisnjaci[0])}</div>
|
||
<div class="k">Zadnji godišnjak</div><div class="v">${esc(d.godisnjak_zadnji||godisnjaci[godisnjaci.length-1])}</div>
|
||
<div class="k">Sve godine</div><div class="v">${godisnjaci.map(g=>'<span class="tag b">'+esc(g)+'</span>').join(' ')}</div>
|
||
</div>` : '<div class="empty">Nema podataka o godišnjacima</div>'}
|
||
</div>
|
||
|
||
<div id="p-nag" class="ptab" style="display:none">
|
||
${nagrade.length ? `<div style="overflow-x:auto"><table>
|
||
<thead><tr><th>Godina</th><th>Nagrada</th><th>Razina</th></tr></thead>
|
||
<tbody>${nagrade.map(n => `
|
||
<tr class="no-click">
|
||
<td>${esc(n.godina||'')}</td>
|
||
<td>${esc(n.naziv||n.nagrada||'')}</td>
|
||
<td>${esc(n.razina||'')}</td>
|
||
</tr>`).join('')}
|
||
</tbody>
|
||
</table></div>` : '<div class="empty">Nema zabilježenih nagrada</div>'}
|
||
</div>
|
||
|
||
${enrichBlock('sportas', d.id)}
|
||
`;
|
||
openPanel('Sportaš · '+(d.ime||'')+' '+(d.prezime||''), html);
|
||
}
|
||
function switchPlayerTab(el, tabId){
|
||
const parent = el.parentElement;
|
||
parent.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
|
||
el.classList.add('active');
|
||
parent.parentElement.querySelectorAll('.ptab').forEach(t=>t.style.display='none');
|
||
const target = document.getElementById(tabId);
|
||
if(target) target.style.display='block';
|
||
}
|
||
|
||
//=========== FINANCIJE ===========
|
||
async function loadFinancije(){
|
||
const root = $('#pg-financije');
|
||
root.innerHTML = `
|
||
<div class="toolbar">
|
||
<select id="fi-god">
|
||
<option value="2026">2026</option>
|
||
<option value="2025">2025</option>
|
||
<option value="2024">2024</option>
|
||
</select>
|
||
<input type="search" id="fi-q" placeholder="🔍 Pretraži korisnika ili sport…">
|
||
<div class="toggle">
|
||
<button id="fi-card" class="${_state.viewFinancije==='card'?'active':''}" onclick="setFinancijeView('card')">Kartice</button>
|
||
<button id="fi-table-btn" class="${_state.viewFinancije==='table'?'active':''}" onclick="setFinancijeView('table')">Tablica</button>
|
||
</div>
|
||
<span class="tb-s" id="fi-cnt"></span>
|
||
</div>
|
||
<div id="fi-kpi"></div>
|
||
<div class="row-2" style="margin-top:14px">
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">📊 Raspodjela po sportovima</div></div>
|
||
<div class="chart-box"><canvas id="chSport"></canvas></div>
|
||
</div>
|
||
<div class="card" id="fi-top"></div>
|
||
</div>
|
||
<div class="card" style="margin-top:14px">
|
||
<div class="card-h"><div class="card-t">📋 Svi primatelji</div></div>
|
||
<div id="fi-table"></div>
|
||
</div>
|
||
`;
|
||
$('#fi-god').addEventListener('change', refreshFinancije);
|
||
$('#fi-q').addEventListener('input', debounce(refreshFinancije, 200));
|
||
refreshFinancije();
|
||
}
|
||
async function refreshFinancije(){
|
||
const god = $('#fi-god').value;
|
||
const q = ($('#fi-q').value || '').toLowerCase().trim();
|
||
const [analytics, byyear] = await Promise.all([
|
||
api('/v2/analytics/proracun-sport?godina='+god),
|
||
api('/v2/potpore/by-year?godina='+god)
|
||
]);
|
||
const total = (analytics && analytics.total) || (byyear && byyear.total) || 0;
|
||
const poSportu = (analytics && analytics.po_sportu) || [];
|
||
let rows = (byyear && byyear.results) || [];
|
||
if(q) rows = rows.filter(r => (r.korisnik||'').toLowerCase().includes(q) || (r.sport||'').toLowerCase().includes(q));
|
||
|
||
$('#fi-kpi').innerHTML = `
|
||
<div class="kpi-grid">
|
||
<div class="kpi"><div class="kpi-l">Ukupno ${god}</div><div class="kpi-v">${fmtEur(total)}</div></div>
|
||
<div class="kpi b"><div class="kpi-l">Primatelja</div><div class="kpi-v">${rows.length}</div></div>
|
||
<div class="kpi g"><div class="kpi-l">Sportova</div><div class="kpi-v">${poSportu.filter(x=>x.sport).length}</div></div>
|
||
<div class="kpi"><div class="kpi-l">Najveća stavka</div><div class="kpi-v" style="font-size:18px">${rows.length?fmtEur(Math.max.apply(null, rows.map(r=>Number(r.iznos_eur||0)))):'—'}</div></div>
|
||
</div>
|
||
`;
|
||
$('#fi-cnt').textContent = rows.length+' primatelja';
|
||
|
||
const top10 = rows.slice().sort((a,b)=>Number(b.iznos_eur||0)-Number(a.iznos_eur||0)).slice(0,10);
|
||
$('#fi-top').innerHTML = `
|
||
<div class="card-h"><div class="card-t">🏆 Top 10 primatelja</div></div>
|
||
<div style="overflow-x:auto;max-height:340px;overflow-y:auto"><table>
|
||
<thead><tr><th>#</th><th>Korisnik</th><th class="num">Iznos</th></tr></thead>
|
||
<tbody>${top10.map((r,i) => `
|
||
<tr class="no-click">
|
||
<td>${i+1}</td>
|
||
<td>${esc(r.korisnik)}</td>
|
||
<td class="num"><b>${fmtEur(r.iznos_eur)}</b></td>
|
||
</tr>`).join('')}</tbody>
|
||
</table></div>
|
||
`;
|
||
|
||
const sportData = poSportu.filter(x=>x.sport).slice(0, 12);
|
||
drawFinancijeChart(sportData);
|
||
|
||
let sortedRows = rows.slice();
|
||
if(_sort.financije) sortedRows = sortRows(sortedRows, _sort.financije.key, _sort.financije.dir);
|
||
|
||
if(_state.viewFinancije === 'card'){
|
||
$('#fi-table').innerHTML = '<div class="grid">'+sortedRows.map(r => `
|
||
<div class="entity" onclick='openPrimateljDetail(${JSON.stringify(r).replace(/'/g,"'")})'>
|
||
${r.source_url?'<div class="et-tag">📄 PDF</div>':''}
|
||
<div class="et">${esc(r.korisnik)}</div>
|
||
<div class="es">${txt(r.sport,'—')} · ${txt(r.vrsta,'')}</div>
|
||
<div class="em">
|
||
<span><b style="color:var(--pgz-gold)">${fmtEur(r.iznos_eur)}</b></span>
|
||
<span>${esc(r.godina||'')}</span>
|
||
</div>
|
||
</div>`).join('')+'</div>';
|
||
} else {
|
||
$('#fi-table').innerHTML = `<div style="overflow-x:auto"><table>
|
||
<thead><tr><th>#</th>${sortHeader('financije','korisnik','Korisnik','')}${sortHeader('financije','sport','Sport','')}${sortHeader('financije','vrsta','Vrsta','')}${sortHeader('financije','iznos_eur','Iznos','num')}${sortHeader('financije','izvor','Izvor','')}<th>PDF</th></tr></thead>
|
||
<tbody>${sortedRows.map((r,i) => `
|
||
<tr onclick='openPrimateljDetail(${JSON.stringify(r).replace(/'/g,"'")})'>
|
||
<td>${i+1}</td>
|
||
<td><b>${esc(r.korisnik)}</b></td>
|
||
<td>${txt(r.sport)}</td>
|
||
<td>${txt(r.vrsta)}</td>
|
||
<td class="num"><b>${fmtEurFull(r.iznos_eur)}</b></td>
|
||
<td>${txt(r.izvor)}</td>
|
||
<td>${r.source_url?'<a href="'+esc(r.source_url)+'" target="_blank" onclick="event.stopPropagation()">📄</a>':'—'}</td>
|
||
</tr>`).join('')}
|
||
</tbody>
|
||
</table></div>`;
|
||
}
|
||
}
|
||
function setFinancijeView(v){
|
||
_state.viewFinancije = v;
|
||
const cb = $('#fi-card'), tb = $('#fi-table-btn');
|
||
if(cb) cb.classList.toggle('active', v==='card');
|
||
if(tb) tb.classList.toggle('active', v==='table');
|
||
refreshFinancije();
|
||
}
|
||
function drawFinancijeChart(data){
|
||
const ctx = $('#chSport');
|
||
if(!ctx) return;
|
||
if(_financijeChart){ _financijeChart.destroy(); _financijeChart=null; }
|
||
const colors = ['#003087','#004CC4','#F4C430','#00e88f','#00c8e8','#ff2d55','#f59e0b','#a855f7','#ec4899','#14b8a6','#84cc16','#f97316'];
|
||
_financijeChart = new Chart(ctx, {
|
||
type:'doughnut',
|
||
data:{
|
||
labels: data.map(x=>x.sport),
|
||
datasets:[{
|
||
data: data.map(x=>x.ukupno),
|
||
backgroundColor: colors.slice(0, data.length),
|
||
borderColor: '#0d1021',
|
||
borderWidth:2
|
||
}]
|
||
},
|
||
options:{
|
||
responsive:true, maintainAspectRatio:false,
|
||
plugins:{
|
||
legend:{position:'right', labels:{color:'#e2e6f0', font:{size:10}, boxWidth:10}},
|
||
tooltip:{callbacks:{label:c => c.label+': €'+Number(c.parsed).toLocaleString('hr-HR')}}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
//=========== OBJEKTI ===========
|
||
async function loadObjekti(){
|
||
const root = $('#pg-objekti');
|
||
if(!_cache.objekti){
|
||
root.innerHTML = '<div class="loading">Učitavanje objekata…</div>';
|
||
const d = await api('/sportski-objekti');
|
||
if(!d){ root.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
|
||
_cache.objekti = d.rows || (Array.isArray(d) ? d : []);
|
||
}
|
||
renderObjektiShell();
|
||
applyObjektiFilter();
|
||
}
|
||
function renderObjektiShell(){
|
||
const root = $('#pg-objekti');
|
||
const tipovi = Array.from(new Set((_cache.objekti||[]).map(o=>o.tip).filter(Boolean))).sort();
|
||
const grads = Array.from(new Set((_cache.objekti||[]).map(o=>o.grad).filter(Boolean))).sort();
|
||
root.innerHTML = `
|
||
<div class="toolbar">
|
||
<input type="search" id="ob-q" placeholder="🔍 Pretraži objekt…">
|
||
<select id="ob-tip"><option value="">Svi tipovi</option>${tipovi.map(t=>'<option value="'+esc(t)+'">'+esc(t)+'</option>').join('')}</select>
|
||
<select id="ob-grad"><option value="">Svi gradovi</option>${grads.map(g=>'<option value="'+esc(g)+'">'+esc(g)+'</option>').join('')}</select>
|
||
<label><input type="checkbox" id="ob-geo"> Samo s koordinatama</label>
|
||
<div class="toggle">
|
||
<button id="ob-card" class="${_state.viewObjekti==='card'?'active':''}" onclick="setObjektiView('card')">Kartice</button>
|
||
<button id="ob-table" class="${_state.viewObjekti==='table'?'active':''}" onclick="setObjektiView('table')">Tablica</button>
|
||
</div>
|
||
<span class="tb-s" id="ob-cnt"></span>
|
||
</div>
|
||
<div id="ob-out"></div>
|
||
`;
|
||
$('#ob-q').addEventListener('input', debounce(applyObjektiFilter, 200));
|
||
$('#ob-tip').addEventListener('change', applyObjektiFilter);
|
||
$('#ob-grad').addEventListener('change', applyObjektiFilter);
|
||
$('#ob-geo').addEventListener('change', applyObjektiFilter);
|
||
}
|
||
function setObjektiView(v){
|
||
_state.viewObjekti = v;
|
||
$('#ob-card').classList.toggle('active', v==='card');
|
||
$('#ob-table').classList.toggle('active', v==='table');
|
||
applyObjektiFilter();
|
||
}
|
||
function applyObjektiFilter(){
|
||
const q = (($('#ob-q')?$('#ob-q').value:'') || '').toLowerCase().trim();
|
||
const tip = $('#ob-tip') ? $('#ob-tip').value : '';
|
||
const grad = $('#ob-grad') ? $('#ob-grad').value : '';
|
||
const geo = $('#ob-geo') ? $('#ob-geo').checked : false;
|
||
let rows = _cache.objekti || [];
|
||
if(q) rows = rows.filter(o => (o.naziv||'').toLowerCase().includes(q) || (o.upravitelj||'').toLowerCase().includes(q));
|
||
if(tip) rows = rows.filter(o => o.tip===tip);
|
||
if(grad) rows = rows.filter(o => o.grad===grad);
|
||
if(geo) rows = rows.filter(o => o.lat && o.lng);
|
||
if(_sort.objekti) rows = sortRows(rows, _sort.objekti.key, _sort.objekti.dir);
|
||
$('#ob-cnt').textContent = rows.length+' objekata';
|
||
$('#ob-out').innerHTML = _state.viewObjekti==='card' ? renderObjektiGrid(rows) : renderObjektiTable(rows);
|
||
}
|
||
function renderObjektiGrid(rows){
|
||
if(!rows.length) return '<div class="empty">Nema rezultata</div>';
|
||
return '<div class="grid">'+rows.map(o => `
|
||
<div class="entity" onclick="openObjekt(${o.id})">
|
||
${o.lat&&o.lng?'<div class="et-tag">📍 GEO</div>':''}
|
||
<div class="et">${esc(o.naziv)}</div>
|
||
<div class="es">${txt(o.tip,'—')} · ${txt(o.grad,'—')}</div>
|
||
<div class="em">
|
||
${o.upravitelj?'<span>'+esc(o.upravitelj)+'</span>':''}
|
||
${o.kapacitet?'<span><b>'+fmtNum(o.kapacitet)+'</b> mjesta</span>':''}
|
||
${o.izgradeno?'<span>est. <b>'+esc(o.izgradeno)+'</b></span>':''}
|
||
</div>
|
||
</div>`).join('')+'</div>';
|
||
}
|
||
function renderObjektiTable(rows){
|
||
if(!rows.length) return '<div class="empty">Nema rezultata</div>';
|
||
return `<div class="card" style="padding:0;overflow-x:auto"><table>
|
||
<thead><tr>${sortHeader('objekti','naziv','Naziv','')}${sortHeader('objekti','tip','Tip','')}${sortHeader('objekti','grad','Grad','')}${sortHeader('objekti','upravitelj','Upravitelj','')}${sortHeader('objekti','izgradeno','Izgrađeno','num')}${sortHeader('objekti','lat','GPS','')}</tr></thead>
|
||
<tbody>${rows.map(o => `
|
||
<tr onclick="openObjekt(${o.id})">
|
||
<td><b>${esc(o.naziv)}</b></td>
|
||
<td>${txt(o.tip)}</td>
|
||
<td>${txt(o.grad)}</td>
|
||
<td>${txt(o.upravitelj)}</td>
|
||
<td class="num">${txt(o.izgradeno)}</td>
|
||
<td>${o.lat&&o.lng?'<span class="tag gr">📍</span>':'—'}</td>
|
||
</tr>`).join('')}</tbody>
|
||
</table></div>`;
|
||
}
|
||
function openObjekt(id){
|
||
const o = (_cache.objekti||[]).find(x => x.id===id);
|
||
if(!o){ openPanel('Objekt', '<div class="empty">Objekt nije pronađen</div>'); return; }
|
||
const mapUrl = (o.lat && o.lng) ? 'https://www.google.com/maps/search/?api=1&query='+o.lat+','+o.lng : null;
|
||
const embedUrl = (o.lat && o.lng) ? 'https://www.google.com/maps?q='+encodeURIComponent((o.naziv||'')+', '+(o.grad||'')+', PGŽ')+'&ll='+o.lat+','+o.lng+'&z=17&output=embed' : null;
|
||
const html = `
|
||
<div class="card-h" style="border:0;padding:0;margin-bottom:14px">
|
||
<div>
|
||
<div style="font-size:18px;font-weight:800;color:var(--t0)">${esc(o.naziv)}</div>
|
||
<div style="font-size:12px;color:var(--t2);margin-top:4px">${txt(o.tip,'—')} · ${txt(o.grad,'')}</div>
|
||
</div>
|
||
</div>
|
||
${embedUrl?'<iframe class="iframe-map" style="height:240px" src="'+embedUrl+'" loading="lazy"></iframe>':''}
|
||
<div class="card" style="margin-top:14px">
|
||
<div class="card-h"><div class="card-t">📋 Detalji</div></div>
|
||
<div class="kv">
|
||
<div class="k">Tip</div><div class="v">${txt(o.tip)}</div>
|
||
<div class="k">Adresa</div><div class="v">${txt(o.adresa)}</div>
|
||
<div class="k">Grad</div><div class="v">${txt(o.grad)}</div>
|
||
<div class="k">Upravitelj</div><div class="v">${txt(o.upravitelj)}</div>
|
||
<div class="k">OIB</div><div class="v">${txt(o.upravitelj_oib)}</div>
|
||
<div class="k">Kapacitet</div><div class="v">${o.kapacitet?fmtNum(o.kapacitet)+' mjesta':'—'}</div>
|
||
<div class="k">Veličina</div><div class="v">${txt(o.veličina)}</div>
|
||
<div class="k">Sportovi</div><div class="v">${(o.sportovi||[]).map(s=>'<span class="tag b">'+esc(s)+'</span>').join(' ')||'—'}</div>
|
||
<div class="k">Izgrađeno</div><div class="v">${txt(o.izgradeno)}</div>
|
||
<div class="k">Obnovljeno</div><div class="v">${txt(o.obnovljeno_god)}</div>
|
||
<div class="k">Natkriven</div><div class="v">${o.natkrita?'DA':'NE'}</div>
|
||
<div class="k">Web</div><div class="v">${o.web?'<a href="'+esc(o.web)+'" target="_blank">'+esc(o.web)+'</a>':'—'}</div>
|
||
<div class="k">Koordinate</div><div class="v">${(o.lat&&o.lng)?'<a href="'+mapUrl+'" target="_blank">'+o.lat.toFixed(5)+', '+o.lng.toFixed(5)+' ↗</a>':'—'}</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
openPanel('Objekt · '+o.naziv, html);
|
||
}
|
||
|
||
//=========== MANIFESTACIJE ===========
|
||
async function loadManifestacije(){
|
||
const root = $('#pg-manifestacije');
|
||
if(!_cache.manifestacije){
|
||
root.innerHTML = '<div class="loading">Učitavanje manifestacija…</div>';
|
||
const d = await api('/manifestacije-full');
|
||
if(!d){ root.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
|
||
_cache.manifestacije = d.rows || (Array.isArray(d) ? d : []);
|
||
}
|
||
renderManifShell();
|
||
applyManifFilter();
|
||
}
|
||
function renderManifShell(){
|
||
const root = $('#pg-manifestacije');
|
||
const razine = Array.from(new Set((_cache.manifestacije||[]).map(m=>m.razina).filter(Boolean))).sort();
|
||
root.innerHTML = `
|
||
<div class="toolbar">
|
||
<input type="search" id="mn-q" placeholder="🔍 Pretraži manifestaciju…">
|
||
<select id="mn-raz"><option value="">Sve razine</option>${razine.map(r=>'<option value="'+esc(r)+'">'+esc(r)+'</option>').join('')}</select>
|
||
<div class="toggle">
|
||
<button id="mn-card" class="${_state.viewManif==='card'?'active':''}" onclick="setManifView('card')">Kartice</button>
|
||
<button id="mn-table" class="${_state.viewManif==='table'?'active':''}" onclick="setManifView('table')">Tablica</button>
|
||
</div>
|
||
<span class="tb-s" id="mn-cnt"></span>
|
||
</div>
|
||
<div id="mn-out"></div>
|
||
`;
|
||
$('#mn-q').addEventListener('input', debounce(applyManifFilter, 200));
|
||
$('#mn-raz').addEventListener('change', applyManifFilter);
|
||
}
|
||
function setManifView(v){
|
||
_state.viewManif = v;
|
||
$('#mn-card').classList.toggle('active', v==='card');
|
||
$('#mn-table').classList.toggle('active', v==='table');
|
||
applyManifFilter();
|
||
}
|
||
function applyManifFilter(){
|
||
const q = (($('#mn-q')?$('#mn-q').value:'') || '').toLowerCase().trim();
|
||
const raz = $('#mn-raz') ? $('#mn-raz').value : '';
|
||
let rows = _cache.manifestacije || [];
|
||
if(q) rows = rows.filter(m => (m.naziv||'').toLowerCase().includes(q) || (m.organizator||'').toLowerCase().includes(q) || (m.mjesto||'').toLowerCase().includes(q));
|
||
if(raz) rows = rows.filter(m => m.razina===raz);
|
||
if(_sort.manifestacije) rows = sortRows(rows, _sort.manifestacije.key, _sort.manifestacije.dir);
|
||
$('#mn-cnt').textContent = rows.length+' manifestacija';
|
||
$('#mn-out').innerHTML = _state.viewManif==='card' ? renderManifGrid(rows) : renderManifTable(rows);
|
||
}
|
||
function renderManifGrid(rows){
|
||
if(!rows.length) return '<div class="empty">Nema rezultata</div>';
|
||
return '<div class="grid">'+rows.map(m => `
|
||
<div class="entity" onclick="openManif(${m.id})">
|
||
${m.razina?'<div class="et-tag">'+esc(m.razina)+'</div>':''}
|
||
<div class="et">${esc(m.naziv)}</div>
|
||
<div class="es">${txt(m.mjesto,'—')}${m.spol_kategorija?' · '+esc(m.spol_kategorija):''}</div>
|
||
<div class="em">
|
||
${m.broj_ucesnika?'<span><b>'+esc(m.broj_ucesnika)+'</b> sudionika</span>':''}
|
||
${m.organizator?'<span>'+esc((m.organizator||'').slice(0,40))+'</span>':''}
|
||
</div>
|
||
</div>`).join('')+'</div>';
|
||
}
|
||
function renderManifTable(rows){
|
||
if(!rows.length) return '<div class="empty">Nema rezultata</div>';
|
||
return `<div class="card" style="padding:0;overflow-x:auto"><table>
|
||
<thead><tr>${sortHeader('manifestacije','naziv','Naziv','')}${sortHeader('manifestacije','mjesto','Mjesto','')}${sortHeader('manifestacije','razina','Razina','')}${sortHeader('manifestacije','organizator','Organizator','')}${sortHeader('manifestacije','broj_ucesnika','Sudionici','')}<th>Link</th></tr></thead>
|
||
<tbody>${rows.map(m => `
|
||
<tr onclick="openManif(${m.id})">
|
||
<td><b>${esc(m.naziv)}</b></td>
|
||
<td>${txt(m.mjesto)}</td>
|
||
<td>${m.razina?'<span class="tag b">'+esc(m.razina)+'</span>':'—'}</td>
|
||
<td>${txt(m.organizator)}</td>
|
||
<td>${txt(m.broj_ucesnika)}</td>
|
||
<td>${m.source_url?'<a href="'+esc(m.source_url)+'" target="_blank">↗</a>':'—'}</td>
|
||
</tr>`).join('')}</tbody>
|
||
</table></div>`;
|
||
}
|
||
function openManif(id){
|
||
const m = (_cache.manifestacije||[]).find(x => x.id===id);
|
||
if(!m){ openPanel('Manifestacija', '<div class="empty">Nije pronađeno</div>'); return; }
|
||
// If we have a source_url, open it directly in a new tab
|
||
if(m.source_url){
|
||
window.open(m.source_url, '_blank', 'noopener');
|
||
return;
|
||
}
|
||
// Otherwise show details + Google search fallback
|
||
const gq = encodeURIComponent((m.naziv||'')+' '+(m.mjesto||'')+' sport');
|
||
const googleUrl = 'https://www.google.com/search?q='+gq;
|
||
const html = `
|
||
<div class="card-h" style="border:0;padding:0;margin-bottom:14px">
|
||
<div>
|
||
<div style="font-size:18px;font-weight:800;color:var(--t0)">${esc(m.naziv)}</div>
|
||
<div style="font-size:12px;color:var(--t2);margin-top:4px">${txt(m.mjesto,'—')} · ${txt(m.razina,'')}</div>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">📋 Detalji</div></div>
|
||
<div class="kv">
|
||
<div class="k">Organizator</div><div class="v">${txt(m.organizator)}</div>
|
||
<div class="k">Razina</div><div class="v">${m.razina?'<span class="tag b">'+esc(m.razina)+'</span>':'—'}</div>
|
||
<div class="k">Sudionici</div><div class="v">${txt(m.broj_ucesnika)}</div>
|
||
<div class="k">Spol/kategorija</div><div class="v">${txt(m.spol_kategorija)}</div>
|
||
<div class="k">Godina od</div><div class="v">${txt(m.godina_od)}</div>
|
||
<div class="k">Mjesto</div><div class="v">${txt(m.mjesto)}</div>
|
||
</div>
|
||
${m.napomena ? '<div style="margin-top:14px;font-size:12px;line-height:1.5;color:var(--t1);padding:10px;background:var(--bg3);border-radius:5px">'+esc(m.napomena)+'</div>' : ''}
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">🌐 Online izvori</div></div>
|
||
<div class="empty" style="padding:14px">Nema poznatog izvornog URL-a. Pokušaj pronaći više informacija online:</div>
|
||
<div style="text-align:center;margin-top:10px">
|
||
<a href="${googleUrl}" target="_blank" class="btn primary" style="display:inline-block;text-decoration:none">🔍 Pretraži na Googleu</a>
|
||
</div>
|
||
</div>
|
||
`;
|
||
openPanel('Manifestacija · '+m.naziv, html);
|
||
}
|
||
|
||
//=========== MREŽA (Network Graph) ===========
|
||
const _mreza = {data:null, sim:null, allNodes:null, allEdges:null, filter:{osoba:'', klub:'', tvrtka:'', tip:''}};
|
||
|
||
async function loadMreza(){
|
||
const root = $('#pg-mreza');
|
||
if(!_mreza.data){
|
||
root.innerHTML = '<div class="loading">Učitavanje grafa entiteta…</div>';
|
||
let resp;
|
||
try{
|
||
const r = await fetch('https://api.rinet.one/api/v1/presenter/graph-real');
|
||
resp = await r.json();
|
||
}catch(e){ console.error('graph fetch error', e); }
|
||
if(!resp || !resp.data){ root.innerHTML='<div class="empty">Greška pri dohvatu graf-podataka</div>'; return; }
|
||
_mreza.data = resp.data;
|
||
_mreza.allNodes = (resp.data.nodes||[]).slice();
|
||
_mreza.allEdges = (resp.data.edges||[]).slice();
|
||
}
|
||
renderMrezaShell();
|
||
renderMrezaGraph();
|
||
}
|
||
|
||
function renderMrezaShell(){
|
||
const root = $('#pg-mreza');
|
||
const types = Array.from(new Set((_mreza.allNodes||[]).map(n=>n.type))).sort();
|
||
const totalN = (_mreza.allNodes||[]).length;
|
||
const totalE = (_mreza.allEdges||[]).length;
|
||
root.innerHTML = `
|
||
<div class="kpi-grid" style="grid-template-columns:repeat(4,1fr);margin-bottom:14px">
|
||
<div class="kpi"><div class="kpi-l">Čvorova</div><div class="kpi-v">${totalN}</div></div>
|
||
<div class="kpi b"><div class="kpi-l">Veza</div><div class="kpi-v">${totalE}</div></div>
|
||
<div class="kpi g"><div class="kpi-l">Osoba</div><div class="kpi-v">${(_mreza.allNodes||[]).filter(n=>n.type==='person').length}</div></div>
|
||
<div class="kpi r"><div class="kpi-l">Tvrtki / entiteta</div><div class="kpi-v">${(_mreza.allNodes||[]).filter(n=>n.type==='entity'||n.type==='supplier').length}</div></div>
|
||
</div>
|
||
|
||
<div class="toolbar" style="margin-bottom:10px">
|
||
<input type="search" id="mr-osoba" placeholder="👤 Osoba…">
|
||
<input type="search" id="mr-klub" placeholder="🏟 Klub / Savez…">
|
||
<input type="search" id="mr-tvrtka" placeholder="🏢 Tvrtka / Entitet…">
|
||
<select id="mr-tip">
|
||
<option value="">Svi tipovi</option>
|
||
${types.map(t=>'<option value="'+esc(t)+'">'+esc(t)+'</option>').join('')}
|
||
</select>
|
||
<button class="btn" onclick="resetMreza()">↺ Reset</button>
|
||
<span class="tb-s" id="mr-cnt"></span>
|
||
</div>
|
||
|
||
<div class="card" style="padding:0;overflow:hidden">
|
||
<div id="mr-graph" style="width:100%;height:640px;background:radial-gradient(ellipse at center,var(--bg2) 0%,var(--bg0) 100%);position:relative">
|
||
<svg id="mr-svg" style="width:100%;height:100%"></svg>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card" style="margin-top:10px">
|
||
<div class="card-h"><div class="card-t">🎨 Legenda</div></div>
|
||
<div style="display:flex;gap:14px;flex-wrap:wrap;font-size:12px">
|
||
<div><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#8b5cf6;vertical-align:middle;margin-right:5px"></span>Osoba</div>
|
||
<div><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#ff4466;vertical-align:middle;margin-right:5px"></span>Entitet (high risk)</div>
|
||
<div><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#00e68a;vertical-align:middle;margin-right:5px"></span>Dobavljač</div>
|
||
<div style="color:var(--t2)">Veličina = risk / promet · Klikni čvor za detalje</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
$('#mr-osoba').addEventListener('input', debounce(applyMrezaFilter, 200));
|
||
$('#mr-klub').addEventListener('input', debounce(applyMrezaFilter, 200));
|
||
$('#mr-tvrtka').addEventListener('input', debounce(applyMrezaFilter, 200));
|
||
$('#mr-tip').addEventListener('change', applyMrezaFilter);
|
||
}
|
||
|
||
function applyMrezaFilter(){
|
||
const osoba = ($('#mr-osoba').value||'').toLowerCase().trim();
|
||
const klub = ($('#mr-klub').value||'').toLowerCase().trim();
|
||
const tvrtka = ($('#mr-tvrtka').value||'').toLowerCase().trim();
|
||
const tip = $('#mr-tip').value;
|
||
|
||
let nodes = (_mreza.allNodes||[]).slice();
|
||
if(osoba) nodes = nodes.filter(n => n.type==='person' && (n.label||'').toLowerCase().includes(osoba) || n.type!=='person');
|
||
if(klub){
|
||
// filter entity/supplier by label match (savezi/klubovi appear as entities)
|
||
nodes = nodes.filter(n => {
|
||
if(n.type==='person') return true;
|
||
return (n.label||'').toLowerCase().includes(klub);
|
||
});
|
||
}
|
||
if(tvrtka){
|
||
nodes = nodes.filter(n => {
|
||
if(n.type==='person') return true;
|
||
return (n.label||'').toLowerCase().includes(tvrtka);
|
||
});
|
||
}
|
||
if(tip) nodes = nodes.filter(n => n.type===tip);
|
||
// Also filter to stronger: if osoba is set, drop persons not matching
|
||
if(osoba) nodes = nodes.filter(n => n.type!=='person' || (n.label||'').toLowerCase().includes(osoba));
|
||
if(klub) nodes = nodes.filter(n => n.type==='person' || (n.label||'').toLowerCase().includes(klub));
|
||
if(tvrtka) nodes = nodes.filter(n => n.type==='person' || (n.label||'').toLowerCase().includes(tvrtka));
|
||
|
||
const ids = new Set(nodes.map(n=>n.id));
|
||
let edges = (_mreza.allEdges||[]).filter(e => ids.has(e.source.id||e.source) && ids.has(e.target.id||e.target));
|
||
// After edge filter, keep only nodes that have at least one edge OR were direct matches
|
||
const used = new Set();
|
||
for(const e of edges){
|
||
used.add(e.source.id||e.source);
|
||
used.add(e.target.id||e.target);
|
||
}
|
||
// If we have any text filter, restrict to nodes that have edges; otherwise keep all
|
||
if(osoba||klub||tvrtka){
|
||
nodes = nodes.filter(n => used.has(n.id));
|
||
}
|
||
|
||
$('#mr-cnt').textContent = nodes.length+' čvorova · '+edges.length+' veza';
|
||
renderMrezaGraph(nodes, edges);
|
||
}
|
||
|
||
function resetMreza(){
|
||
$('#mr-osoba').value = '';
|
||
$('#mr-klub').value = '';
|
||
$('#mr-tvrtka').value = '';
|
||
$('#mr-tip').value = '';
|
||
applyMrezaFilter();
|
||
}
|
||
|
||
function renderMrezaGraph(nodes, edges){
|
||
if(!nodes) nodes = (_mreza.allNodes||[]).slice();
|
||
if(!edges) edges = (_mreza.allEdges||[]).slice();
|
||
const svgEl = document.getElementById('mr-svg');
|
||
if(!svgEl) return;
|
||
const container = document.getElementById('mr-graph');
|
||
const W = container.clientWidth || 800;
|
||
const H = container.clientHeight || 640;
|
||
|
||
if(_mreza.sim){ try{_mreza.sim.stop();}catch(e){} _mreza.sim = null; }
|
||
|
||
const svg = d3.select(svgEl);
|
||
svg.selectAll('*').remove();
|
||
svg.attr('viewBox', '0 0 '+W+' '+H);
|
||
|
||
// Deep-copy so D3 sim doesn't mutate originals
|
||
const N = nodes.map(n => Object.assign({}, n));
|
||
const Nmap = new Map(N.map(n=>[n.id, n]));
|
||
const E = edges.map(e => ({
|
||
source: Nmap.get(e.source.id||e.source) || (e.source.id||e.source),
|
||
target: Nmap.get(e.target.id||e.target) || (e.target.id||e.target),
|
||
color: e.color, size: e.size
|
||
})).filter(e => typeof e.source === 'object' && typeof e.target === 'object');
|
||
|
||
// Zoom/pan
|
||
const g = svg.append('g');
|
||
svg.call(d3.zoom().scaleExtent([0.2, 5]).on('zoom', (ev) => g.attr('transform', ev.transform)));
|
||
|
||
const sim = d3.forceSimulation(N)
|
||
.force('link', d3.forceLink(E).id(d => d.id).distance(d => 60 + 20/(d.size||1)))
|
||
.force('charge', d3.forceManyBody().strength(d => -50 - (d.size||5)*4))
|
||
.force('center', d3.forceCenter(W/2, H/2))
|
||
.force('collide', d3.forceCollide().radius(d => Math.max(6, (d.size||5)*0.7 + 4)));
|
||
_mreza.sim = sim;
|
||
|
||
const link = g.append('g')
|
||
.attr('stroke-opacity', 0.5)
|
||
.selectAll('line').data(E).join('line')
|
||
.attr('stroke', d => d.color || '#283560')
|
||
.attr('stroke-width', d => Math.max(0.4, (d.size||0.4)));
|
||
|
||
const node = g.append('g')
|
||
.selectAll('g').data(N).join('g')
|
||
.style('cursor','pointer')
|
||
.call(d3.drag()
|
||
.on('start', (ev,d) => { if(!ev.active) sim.alphaTarget(0.3).restart(); d.fx=d.x; d.fy=d.y; })
|
||
.on('drag', (ev,d) => { d.fx=ev.x; d.fy=ev.y; })
|
||
.on('end', (ev,d) => { if(!ev.active) sim.alphaTarget(0); d.fx=null; d.fy=null; }))
|
||
.on('click', (ev,d) => openMrezaNode(d));
|
||
|
||
node.append('circle')
|
||
.attr('r', d => Math.max(5, (d.size||5)*0.7))
|
||
.attr('fill', d => d.color || '#004CC4')
|
||
.attr('stroke', '#0d1021')
|
||
.attr('stroke-width', 1.5);
|
||
|
||
node.append('text')
|
||
.text(d => (d.label||'').slice(0,28))
|
||
.attr('x', d => Math.max(6, (d.size||5)*0.7) + 4)
|
||
.attr('y', 4)
|
||
.attr('fill', '#e2e6f0')
|
||
.attr('font-size', '10px')
|
||
.attr('font-family', 'Inter, sans-serif')
|
||
.style('pointer-events','none');
|
||
|
||
node.append('title').text(d => (d.label||'')+' ['+d.type+']');
|
||
|
||
sim.on('tick', () => {
|
||
link.attr('x1', d=>d.source.x).attr('y1', d=>d.source.y)
|
||
.attr('x2', d=>d.target.x).attr('y2', d=>d.target.y);
|
||
node.attr('transform', d => 'translate('+d.x+','+d.y+')');
|
||
});
|
||
|
||
if(!$('#mr-cnt').textContent){
|
||
$('#mr-cnt').textContent = N.length+' čvorova · '+E.length+' veza';
|
||
}
|
||
}
|
||
|
||
function openMrezaNode(n){
|
||
const m = n.meta || {};
|
||
// Find connected nodes
|
||
const id = n.id;
|
||
const edges = (_mreza.allEdges||[]).filter(e => (e.source.id||e.source)===id || (e.target.id||e.target)===id);
|
||
const connectedIds = new Set();
|
||
for(const e of edges){
|
||
const s = e.source.id||e.source;
|
||
const t = e.target.id||e.target;
|
||
if(s===id) connectedIds.add(t); else connectedIds.add(s);
|
||
}
|
||
const connected = (_mreza.allNodes||[]).filter(x => connectedIds.has(x.id));
|
||
|
||
let html = `
|
||
<div class="card-h" style="border:0;padding:0;margin-bottom:14px">
|
||
<div>
|
||
<div style="font-size:18px;font-weight:800;color:var(--t0)">${esc(n.label)}</div>
|
||
<div style="font-size:12px;color:var(--t2);margin-top:4px">
|
||
<span class="tag b">${esc(n.type)}</span>
|
||
${m.risk?'<span class="tag rd">Risk '+m.risk+'</span>':''}
|
||
${m.forensic?'<span class="tag am">Forenzika '+m.forensic+'</span>':''}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
${(m.risk!=null || m.forensic!=null) ? `
|
||
<div class="kpi-grid" style="grid-template-columns:repeat(3,1fr);margin-bottom:14px">
|
||
${m.risk!=null ? '<div class="kpi r"><div class="kpi-l">Risk score</div><div class="kpi-v">'+m.risk+'</div></div>' : ''}
|
||
${m.forensic!=null ? '<div class="kpi"><div class="kpi-l">Forenzički flag</div><div class="kpi-v">'+m.forensic+'</div></div>' : ''}
|
||
${m.winner_contracts!=null ? '<div class="kpi b"><div class="kpi-l">Ugovori (W)</div><div class="kpi-v">'+m.winner_contracts+'</div></div>' : ''}
|
||
</div>` : ''}
|
||
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">📋 Detalji</div></div>
|
||
<div class="kv">
|
||
<div class="k">ID</div><div class="v" style="font-family:var(--mono);font-size:11px">${esc(n.id)}</div>
|
||
<div class="k">Tip</div><div class="v">${esc(n.type)}</div>
|
||
<div class="k">Naziv</div><div class="v">${esc(n.label)}</div>
|
||
${m.oib?'<div class="k">OIB</div><div class="v" style="font-family:var(--mono)">'+esc(m.oib)+'</div>':''}
|
||
${m.city?'<div class="k">Grad</div><div class="v">'+esc(m.city)+'</div>':''}
|
||
${m.buyer_contracts!=null?'<div class="k">Ugovori kao kupac</div><div class="v">'+m.buyer_contracts+'</div>':''}
|
||
${m.buyer_value!=null?'<div class="k">Vrijednost (kupac)</div><div class="v">'+fmtEurFull(m.buyer_value)+'</div>':''}
|
||
${m.winner_contracts!=null?'<div class="k">Ugovori kao dobavljač</div><div class="v">'+m.winner_contracts+'</div>':''}
|
||
${m.total!=null?'<div class="k">Ukupan promet</div><div class="v"><b style="color:var(--pgz-gold)">'+fmtEurFull(m.total)+'</b></div>':''}
|
||
${m.contracts!=null?'<div class="k">Broj ugovora</div><div class="v">'+m.contracts+'</div>':''}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">🔗 Veze (${connected.length})</div></div>
|
||
${connected.length ? '<div style="overflow-x:auto;max-height:300px;overflow-y:auto"><table>'+
|
||
'<thead><tr><th>Tip</th><th>Naziv</th><th>Risk</th></tr></thead>'+
|
||
'<tbody>'+connected.slice(0,80).map(c => `
|
||
<tr onclick="openMrezaNode(${JSON.stringify(c).replace(/"/g,'"')})">
|
||
<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 2024–2026', 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>
|
||
<button class="btn primary" onclick="runForensicScan()">⚡ Pokreni novu analizu</button>
|
||
<span class="tb-s" id="fz-cnt"></span>
|
||
</div>
|
||
|
||
<div id="fz-out"></div>
|
||
`;
|
||
$('#fz-q').addEventListener('input', debounce(applyForenzikaFilter, 200));
|
||
$('#fz-sev').addEventListener('change', applyForenzikaFilter);
|
||
$('#fz-tip').addEventListener('change', applyForenzikaFilter);
|
||
}
|
||
|
||
function applyForenzikaFilter(){
|
||
const q = (($('#fz-q')?$('#fz-q').value:'') || '').toLowerCase().trim();
|
||
const sev = $('#fz-sev') ? $('#fz-sev').value : '';
|
||
const tip = $('#fz-tip') ? $('#fz-tip').value : '';
|
||
|
||
// Combine custom findings + DB alerts into a unified list
|
||
const combined = [];
|
||
for(const c of (_forenzika.custom||[])){
|
||
combined.push({
|
||
_kind: 'custom', id: c.id, severity: c.severity, tip: c.tip,
|
||
naslov: c.naslov, poruka: c.sazetak, klub_id: null, clan_id: null, datum: null,
|
||
raw: c
|
||
});
|
||
}
|
||
for(const a of (_forenzika.alerts||[])){
|
||
combined.push({
|
||
_kind: 'alert', id: a.id, severity: a.razina, tip: a.tip,
|
||
naslov: '['+a.tip+'] '+(a.poruka||'').slice(0,80),
|
||
poruka: a.poruka, klub_id: a.klub_id, clan_id: a.clan_id, datum: a.datum,
|
||
raw: a
|
||
});
|
||
}
|
||
let rows = combined;
|
||
if(q) rows = rows.filter(r => (r.naslov||'').toLowerCase().includes(q) || (r.poruka||'').toLowerCase().includes(q) || (r.tip||'').toLowerCase().includes(q));
|
||
if(sev) rows = rows.filter(r => r.severity===sev);
|
||
if(tip) rows = rows.filter(r => r.tip===tip);
|
||
|
||
$('#fz-cnt').textContent = rows.length+' nalaza';
|
||
const out = $('#fz-out');
|
||
if(!rows.length){ out.innerHTML='<div class="empty">Nema rezultata pri tim filtrima</div>'; return; }
|
||
out.innerHTML = rows.map((r,i) => {
|
||
const sevClass = r.severity==='CRITICAL'?'crit':(r.severity==='HIGH'?'crit':'');
|
||
const sevColor = r.severity==='CRITICAL'?'rd':(r.severity==='HIGH'?'am':'b');
|
||
return `
|
||
<div class="alert-card ${sevClass}" style="cursor:pointer;display:flex;align-items:center;gap:12px;padding:12px 14px" onclick="openForensicDetail('${r._kind}','${r.id}')">
|
||
<div style="font-size:22px;flex-shrink:0">${r.severity==='CRITICAL'?'🔴':r.severity==='HIGH'?'🟠':'🟡'}</div>
|
||
<div style="flex:1;min-width:0">
|
||
<div class="at">${esc(r.naslov)}</div>
|
||
<div class="ad">${esc(r.poruka||'')}</div>
|
||
${r.datum?'<div class="ad">📅 '+fmtDate(r.datum)+'</div>':''}
|
||
</div>
|
||
<div><span class="tag ${sevColor}">${esc(r.severity||'?')}</span></div>
|
||
<div style="color:var(--t4);font-size:18px">›</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
function openForensicDetail(kind, id){
|
||
if(kind === 'custom'){
|
||
const c = (_forenzika.custom||[]).find(x => x.id === id);
|
||
if(!c){ openPanel('Nalaz', '<div class="empty">Nalaz nije pronađen</div>'); return; }
|
||
return renderCustomFindingPanel(c);
|
||
} else {
|
||
const a = (_forenzika.alerts||[]).find(x => String(x.id) === String(id));
|
||
if(!a){ openPanel('Alarm', '<div class="empty">Alarm nije pronađen</div>'); return; }
|
||
return renderAlertPanel(a);
|
||
}
|
||
}
|
||
|
||
function renderCustomFindingPanel(c){
|
||
const sevColor = c.severity==='CRITICAL'?'rd':(c.severity==='HIGH'?'am':'b');
|
||
const html = `
|
||
<div class="card-h" style="border:0;padding:0;margin-bottom:14px">
|
||
<div>
|
||
<div style="font-size:18px;font-weight:800;color:var(--t0)">${esc(c.naslov)}</div>
|
||
<div style="font-size:12px;color:var(--t2);margin-top:4px">
|
||
<span class="tag ${sevColor}">${esc(c.severity)}</span>
|
||
<span class="tag">${esc(c.tip)}</span>
|
||
${c.osoba?'<span class="tag b">'+esc(c.osoba)+'</span>':''}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="kpi-grid" style="grid-template-columns:1fr 1fr 1fr;margin-bottom:14px">
|
||
<div class="kpi r"><div class="kpi-l">Risk Score</div><div class="kpi-v">${c.risk_score||0}<span style="font-size:14px;color:var(--t2)">/100</span></div></div>
|
||
<div class="kpi"><div class="kpi-l">Severity</div><div class="kpi-v" style="font-size:16px">${esc(c.severity)}</div></div>
|
||
<div class="kpi b"><div class="kpi-l">Tip</div><div class="kpi-v" style="font-size:14px">${esc(c.tip)}</div></div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">📖 Uvod u nalaz</div></div>
|
||
<div style="font-size:12px;line-height:1.6;color:var(--t1)">${esc(c.uvod)}</div>
|
||
</div>
|
||
|
||
${c.povezane_entitete && c.povezane_entitete.length ? `
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">🔗 Povezani entiteti (${c.povezane_entitete.length})</div></div>
|
||
<table>
|
||
<thead><tr><th>Tip</th><th>Naziv</th><th>Napomena</th></tr></thead>
|
||
<tbody>${c.povezane_entitete.map(e => `
|
||
<tr class="no-click">
|
||
<td><span class="tag b">${esc(e.tip)}</span></td>
|
||
<td><b>${esc(e.naziv)}</b></td>
|
||
<td>${esc(e.napomena||'')}</td>
|
||
</tr>`).join('')}</tbody>
|
||
</table>
|
||
</div>` : ''}
|
||
|
||
${c.timeline && c.timeline.length ? `
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">📅 Vremenska linija</div></div>
|
||
<div style="display:flex;flex-direction:column;gap:8px">
|
||
${c.timeline.map(t => `
|
||
<div style="display:flex;gap:10px;padding:8px 10px;background:var(--bg3);border-radius:5px;border-left:3px solid var(--pgz-blue2)">
|
||
<div style="font-family:var(--mono);color:var(--pgz-gold);font-weight:700;font-size:11px;min-width:90px">${esc(t.kad)}</div>
|
||
<div style="font-size:12px;color:var(--t1)">${esc(t.sto)}</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>` : ''}
|
||
|
||
${c.breakdown && c.breakdown.length ? `
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">📊 Risk score breakdown</div></div>
|
||
<div style="display:flex;flex-direction:column;gap:8px">
|
||
${c.breakdown.map(b => {
|
||
const pct = Math.min(100, Math.round((b.tezina/100)*100));
|
||
return `
|
||
<div>
|
||
<div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:3px">
|
||
<span style="color:var(--t1)">${esc(b.kriterij)}</span>
|
||
<span style="font-family:var(--mono);color:var(--pgz-gold);font-weight:700">+${b.tezina}</span>
|
||
</div>
|
||
<div style="height:6px;background:var(--bg3);border-radius:3px;overflow:hidden">
|
||
<div style="height:100%;background:linear-gradient(90deg,var(--red),var(--pgz-gold));width:${pct}%"></div>
|
||
</div>
|
||
</div>`;
|
||
}).join('')}
|
||
</div>
|
||
</div>` : ''}
|
||
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">📄 Dokumenti / dokazi</div></div>
|
||
<div class="empty" style="padding:14px">Za ovaj manualni nalaz nisu priloženi PDF dokazi. Pokreni "Obogati podatke" za prikupljanje izvora.</div>
|
||
</div>
|
||
`;
|
||
openPanel('Forenzika · '+c.naslov, html);
|
||
}
|
||
|
||
function renderAlertPanel(a){
|
||
const sevColor = a.razina==='CRITICAL'?'rd':(a.razina==='HIGH'?'am':'b');
|
||
const html = `
|
||
<div class="card-h" style="border:0;padding:0;margin-bottom:14px">
|
||
<div>
|
||
<div style="font-size:18px;font-weight:800;color:var(--t0)">[${esc(a.tip)}]</div>
|
||
<div style="font-size:12px;color:var(--t2);margin-top:4px">
|
||
<span class="tag ${sevColor}">${esc(a.razina)}</span>
|
||
${a.datum?'<span class="tag">📅 '+fmtDate(a.datum)+'</span>':''}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">📖 Opis alarma</div></div>
|
||
<div style="font-size:13px;line-height:1.6;color:var(--t1)">${esc(a.poruka||'')}</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">🔗 Povezani entiteti</div></div>
|
||
<table>
|
||
<tbody>
|
||
${a.klub_id ? '<tr onclick="closePanel();setTimeout(()=>openKlub('+a.klub_id+'),250)"><td><span class="tag b">Klub</span></td><td><b>Klub #'+a.klub_id+'</b></td><td>Klikni za detalje →</td></tr>' : ''}
|
||
${a.clan_id ? '<tr onclick="closePanel();setTimeout(()=>openSportas('+a.clan_id+'),250)"><td><span class="tag b">Sportaš</span></td><td><b>Sportaš #'+a.clan_id+'</b></td><td>Klikni za profil →</td></tr>' : ''}
|
||
</tbody>
|
||
</table>
|
||
${(!a.klub_id && !a.clan_id) ? '<div class="empty" style="padding:14px">Nema povezanih entiteta u alarmu</div>' : ''}
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">📅 Metapodaci</div></div>
|
||
<div class="kv">
|
||
<div class="k">ID</div><div class="v">${esc(a.id)}</div>
|
||
<div class="k">Tip</div><div class="v">${esc(a.tip)}</div>
|
||
<div class="k">Razina</div><div class="v">${esc(a.razina)}</div>
|
||
<div class="k">Datum događaja</div><div class="v">${a.datum?fmtDate(a.datum):'—'}</div>
|
||
<div class="k">Iznos</div><div class="v">${a.iznos!=null?fmtEurFull(a.iznos):'—'}</div>
|
||
<div class="k">Riješeno</div><div class="v">${a.rijeseno?'<span class="tag gr">DA</span>':'<span class="tag rd">NE</span>'}</div>
|
||
<div class="k">Kreirano</div><div class="v">${a.created_at?fmtDate(a.created_at):'—'}</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
openPanel('Alarm #'+a.id, html);
|
||
}
|
||
|
||
async function runForensicScan(){
|
||
const btn = event && event.target;
|
||
if(btn){ btn.disabled = true; btn.textContent = '⏳ Skeniranje…'; }
|
||
const r = await api('/v2/alerts/scan');
|
||
if(btn){ btn.disabled = false; btn.textContent = '⚡ Pokreni novu analizu'; }
|
||
// Reload
|
||
_forenzika.alerts = null;
|
||
await loadForenzika();
|
||
alert(r ? 'Skeniranje pokrenuto. Pronađeno alarma: '+(_forenzika.alerts||[]).length : 'Greška pri pokretanju skeniranja.');
|
||
}
|
||
|
||
//=========== INIT ===========
|
||
function init(){
|
||
restoreSidebar();
|
||
buildNav();
|
||
navTo('dashboard');
|
||
}
|
||
window.addEventListener('DOMContentLoaded', init);
|
||
</script>
|
||
</body>
|
||
</html>
|