Files
pgz-sport/static/index.html.BROKEN.1777884042

8116 lines
457 KiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="hr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
<meta name="theme-color" content="#0A0E1A">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='12' fill='%230d0d0d'/%3E%3Ctext x='32' y='42' font-family='IBM Plex Sans,sans-serif' font-size='28' font-weight='700' fill='%233b82c4' text-anchor='middle'%3EPG%3C/text%3E%3C/svg%3E">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<!-- v=1777465915 -->
<title>PGŽ Sport · Ri.NET</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&family=JetBrains+Mono:wght@500;600&display=swap" rel="stylesheet">
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&family=IBM+Plex+Sans:wght@300;400;500;600;700&display=swap');
/* ═══════════════════════════════════════════
PGŽ SPORT — RI.NET UNIFIED TOKENS
Synced with /opt/rinet-v4/app/src/index.css
v2.0.0 | 29.04.2026
═══════════════════════════════════════════ */
:root {
/* === MASTER TOKENS (klasik-aligned) === */
--bg: #0d0d0d;
--bg2: #141414;
--bg3: #1a1a1a;
--bg4: #1a2332;
--bg5: #243044;
--border: #1e293b;
--border2: rgba(30,41,59,0.6);
--border3: rgba(56, 97, 150, 0.4);
--text: #94a3b8;
--text2: #cbd5e1;
--text3: #c0c0d0;
--text-bright: #f1f5f9;
--text-dim: #6B7A99;
--accent: #3b82c4;
--accent2: #2563a0;
--accent-glow: rgba(59, 130, 196, 0.12);
--green: #22c55e;
--red: #ef4444;
--amber: #f59e0b;
--cyan: #06b6d4;
--sans: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, sans-serif;
--mono: 'JetBrains Mono', 'Fira Code', SF Mono, Consolas, monospace;
--radius: 4px;
--radius-lg: 6px;
--chart1: #3b82c4;
--chart2: #1d6fa8;
--chart3: #0f5a8e;
--chart4: #064572;
--gradient-main: linear-gradient(135deg, #1a3a5c, #0f2440);
--gradient-accent: linear-gradient(90deg, #2563a0, #1d6fa8);
/* === LEGACY ALIASES (sport-specific, dash variants) === */
--bg-2: var(--bg2);
--bg-3: var(--bg3);
--border-2: var(--border2);
--accent-2: var(--accent2);
--text-2: var(--text);
--text-3: var(--text-dim);
/* === SEMANTIC ALIASES === */
--ok: var(--green);
--warn: var(--amber);
--crit: var(--red);
/* === DOMAIN COLORS (kept for sport visual variety) === */
--gold: var(--amber);
--purple: #A78BFA;
--pink: #F472B6;
/* === LEGACY RADIUS === */
--r: var(--radius-lg);
--r-sm: var(--radius);
/* === PANEL ALIASES (sport had panel/panel-2) === */
--panel: var(--bg2);
--panel-2: var(--bg4);
}
/* ═══ MASTER UTILITY CLASSES — append, sport overrides may follow ═══ */
.ri-card {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 12px;
transition: border-color 0.15s;
}
.ri-card:hover { border-color: var(--border2); }
.ri-glass {
background: rgba(8, 12, 20, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
}
.ri-tbl { width:100%; border-collapse:collapse; font-family:var(--mono); font-size:11px; }
.ri-tbl th { text-align:left; padding:6px 8px; font-weight:500; font-size:10px; text-transform:uppercase; letter-spacing:0.5px; color:var(--text3); border-bottom:1px solid var(--border2); background:var(--bg3); }
.ri-tbl td { padding:4px 8px; border-bottom:1px solid var(--border); white-space:nowrap; color:var(--text); }
.ri-tbl tr:hover td { background: var(--accent-glow); }
.ri-btn { font-family:var(--sans); font-size:11px; font-weight:500; padding:6px 12px; border-radius:var(--radius); border:1px solid var(--border2); background:var(--bg3); color:var(--text); cursor:pointer; transition:all 0.15s; letter-spacing:0.02em; }
.ri-btn:hover { background:var(--bg4); border-color:var(--border3); }
.ri-btn-primary { background:var(--accent2); border-color:var(--accent); color:white; }
.ri-btn-primary:hover { background:var(--accent); }
.ri-btn-ghost { background:transparent; border-color:var(--border); color:var(--text2); }
.ri-kpi-value { font-family:var(--mono); font-weight:700; font-size:20px; color:var(--text-bright); letter-spacing:-0.02em; }
.ri-kpi-label { font-size:9px; text-transform:uppercase; letter-spacing:0.8px; color:var(--text3); margin-top:2px; }
.risk { font-family:var(--mono); font-size:9px; font-weight:700; padding:2px 6px; border-radius:2px; }
.risk-critical { background:rgba(239,68,68,0.12); color:var(--red); }
.risk-high { background:rgba(212,160,23,0.12); color:var(--amber); }
.risk-medium { background:rgba(6,182,212,0.12); color:var(--cyan); }
.risk-low { background:rgba(34,197,94,0.12); color:var(--green); }
/* ═══ ICON — Lucide stroke style ═══ */
.ico-svg, .ri-ico {
stroke: currentColor;
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
fill: none;
flex-shrink: 0;
opacity: 0.7;
}
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
html, body {
height: 100%;
background: var(--bg);
color: var(--text);
font-family: 'IBM Plex Sans', 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
font-size: 14px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
overflow: hidden;
}
.mono { font-family: 'JetBrains Mono', SF Mono, Consolas, monospace; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 4px; }
/* === LAYOUT === */
.app { display: flex; height: 100vh; height: 100dvh; }
/* SIDEBAR DESKTOP */
.sidebar {
width: 260px;
background: var(--bg-2);
border-right: 1px solid var(--border);
display: flex; flex-direction: column;
flex-shrink: 0;
}
.sb-head { padding: 16px 18px; border-bottom: 1px solid var(--border); }
.brand { display: flex; align-items: center; gap: 11px; }
.brand-mark {
width: 34px; height: 34px;
background: linear-gradient(135deg, var(--accent), var(--gold));
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
color: white; font-weight: 700; font-size: 13px; letter-spacing: -0.5px;
box-shadow: 0 2px 12px rgba(59,130,196,0.4);
}
.brand-text h1 { font-size: 15px; font-weight: 700; letter-spacing: -0.3px; line-height: 1.2; }
.brand-text p { font-size: 10px; color: var(--text-3); text-transform: uppercase; letter-spacing: 1.2px; margin-top: 2px; }
.role-pill {
margin-top: 12px; display: inline-flex; align-items: center; gap: 6px;
padding: 4px 10px; border-radius: 100px;
font-size: 10px; font-weight: 700; letter-spacing: 0.7px;
cursor: pointer; transition: 150ms;
}
.role-pill::before { content: ''; width: 5px; height: 5px; border-radius: 50%; }
.role-pill.viewer { background: rgba(59,130,196,0.12); color: var(--accent); border: 1px solid rgba(59,130,196,0.3); }
.role-pill.viewer::before { background: var(--accent); }
.role-pill.admin { background: rgba(239,68,68,0.12); color: var(--crit); border: 1px solid rgba(239,68,68,0.3); }
.role-pill.admin::before { background: var(--crit); }
.nav { flex: 1; overflow-y: auto; padding: 8px 0 16px; }
.nav-sec {
padding: 12px 18px 4px;
color: var(--text-3); text-transform: uppercase;
font-size: 10px; font-weight: 700; letter-spacing: 1.2px;
}
.nav-i {
display: flex; align-items: center; gap: 11px;
padding: 9px 18px;
color: var(--text-2); cursor: pointer;
transition: 150ms;
border-left: 2px solid transparent;
font-size: 13px; font-weight: 500;
}
.nav-i:hover { background: var(--panel); color: var(--text); }
.nav-i.active {
background: linear-gradient(90deg, rgba(59,130,196,0.14) 0%, transparent 100%);
color: var(--accent);
border-left-color: var(--accent);
}
.nav-i .ico { width: 16px; height: 16px; flex-shrink: 0; }
.nav-i .b {
margin-left: auto; background: var(--crit); color: white;
font-size: 9px; font-weight: 700; padding: 2px 6px;
border-radius: 100px; min-width: 18px; text-align: center;
}
.nav-i .b.warn { background: var(--warn); color: var(--bg); }
.sb-foot {
padding: 11px 18px; border-top: 1px solid var(--border);
color: var(--text-3); font-size: 10px; line-height: 1.5;
}
/* MAIN */
.main { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
.topbar {
background: var(--bg-2);
border-bottom: 1px solid var(--border);
padding: 12px 20px;
display: flex; justify-content: space-between; align-items: center;
gap: 12px;
flex-shrink: 0;
}
.tb-title { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
.tb-bc { font-size: 10px; color: var(--text-3); text-transform: uppercase; letter-spacing: 1.2px; font-weight: 700; }
.tb-title h2 { font-size: 18px; font-weight: 700; letter-spacing: -0.3px; line-height: 1.3; }
.tb-meta { color: var(--text-3); font-size: 11px; display: flex; gap: 6px; align-items: center; flex-shrink: 0; }
.live-dot {
display: inline-block; width: 6px; height: 6px;
background: var(--ok); border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(45,212,191,0.55); }
50% { box-shadow: 0 0 0 5px rgba(45,212,191,0); }
}
.menu-btn {
display: none;
width: 36px; height: 36px;
border: 1px solid var(--border); background: var(--panel);
color: var(--text); border-radius: 8px;
align-items: center; justify-content: center;
cursor: pointer;
}
.content { flex: 1; overflow-y: auto; padding: 18px 20px 80px; }
.content > .inner { max-width: 1500px; margin: 0 auto; }
/* GRIDS */
.grid { display: grid; gap: 12px; }
.g2 { grid-template-columns: repeat(2, 1fr); }
.g3 { grid-template-columns: repeat(3, 1fr); }
.g4 { grid-template-columns: repeat(4, 1fr); }
/* CARDS */
.card {
background: var(--panel); border: 1px solid var(--border);
border-radius: var(--r); padding: 14px 16px;
transition: border-color 150ms;
}
.card.acc { border-left: 3px solid var(--accent); }
.card.gold { border-left: 3px solid var(--gold); }
.card.ok { border-left: 3px solid var(--ok); }
.card.warn { border-left: 3px solid var(--warn); }
.card.crit { border-left: 3px solid var(--crit); }
.stat-l {
color: var(--text-3); font-size: 10px;
text-transform: uppercase; letter-spacing: 1px; font-weight: 700;
margin-bottom: 6px;
display: flex; align-items: center; gap: 5px;
}
.stat-v {
font-family: 'JetBrains Mono', monospace;
font-size: 26px; font-weight: 600; letter-spacing: -0.5px; line-height: 1.05;
}
.stat-v.sm { font-size: 18px; }
.stat-v.lg { font-size: 30px; }
.stat-d { font-size: 10.5px; margin-top: 5px; color: var(--text-3); }
.stat-d.up { color: var(--ok); }
.stat-d.down { color: var(--crit); }
.card.crit .stat-v { color: var(--crit); }
.card.warn .stat-v { color: var(--warn); }
.card.ok .stat-v { color: var(--ok); }
.card.acc .stat-v { color: var(--accent); }
.card.gold .stat-v { color: var(--gold); }
.ct {
font-size: 11px; font-weight: 700; color: var(--text);
text-transform: uppercase; letter-spacing: 1px;
margin-bottom: 12px; padding-bottom: 10px;
border-bottom: 1px solid var(--border);
display: flex; justify-content: space-between; align-items: center; gap: 8px;
}
.ct .meta { font-size: 10px; color: var(--text-3); font-weight: 500; text-transform: none; letter-spacing: 0; }
/* SECTION */
.sect {
font-size: 10px; color: var(--text-3);
text-transform: uppercase; letter-spacing: 1.4px; font-weight: 700;
margin: 22px 0 10px;
display: flex; align-items: center; gap: 10px;
}
.sect::after { content: ''; flex: 1; height: 1px; background: var(--border); }
.sect:first-child { margin-top: 0; }
/* FILTER BAR */
.fbar {
background: var(--panel); border: 1px solid var(--border);
border-radius: var(--r); padding: 12px 14px;
margin-bottom: 14px;
}
.fbar-t {
font-size: 10px; color: var(--text-3);
text-transform: uppercase; letter-spacing: 1.2px; font-weight: 700;
margin-bottom: 8px;
}
.fgrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 8px;
}
.fitem label {
display: block; font-size: 9px; color: var(--text-3);
text-transform: uppercase; letter-spacing: 0.6px; margin-bottom: 3px; font-weight: 700;
}
/* INPUTS */
.inp, select {
background: var(--bg-2); color: var(--text);
border: 1px solid var(--border);
padding: 8px 11px; border-radius: var(--r-sm);
font-size: 13px; outline: none; font-family: inherit;
width: 100%;
transition: 150ms;
-webkit-appearance: none;
appearance: none;
}
select {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%236B7A99' d='M6 8L0 0h12z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 32px;
}
.inp:focus, select:focus { border-color: var(--accent); }
.inp.flex { flex: 1; min-width: 180px; }
.btn {
background: var(--accent); color: white; border: none;
padding: 9px 14px; border-radius: var(--r-sm);
font-size: 12.5px; font-weight: 600; cursor: pointer;
transition: 150ms; font-family: inherit;
display: inline-flex; align-items: center; gap: 6px;
}
.btn:hover { background: var(--accent-2); }
.btn.sec { background: var(--panel-2); color: var(--text); border: 1px solid var(--border); }
.btn.sec:hover { border-color: var(--accent); }
.btn.warn { background: var(--warn); color: var(--bg); }
.btn.crit { background: var(--crit); }
.btn.sm { padding: 5px 10px; font-size: 11px; }
.toolbar { display: flex; gap: 8px; margin-bottom: 14px; align-items: center; flex-wrap: wrap; }
/* TABLES */
.tbl-wrap {
background: var(--panel); border: 1px solid var(--border);
border-radius: var(--r);
overflow: auto;
-webkit-overflow-scrolling: touch;
}
table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
thead th {
background: var(--bg-3); color: var(--text-3);
padding: 10px 12px; text-align: left;
font-weight: 700; font-size: 9.5px;
text-transform: uppercase; letter-spacing: 1px;
cursor: pointer; user-select: none;
border-bottom: 1px solid var(--border);
white-space: nowrap;
position: sticky; top: 0; z-index: 5;
}
thead th:hover { color: var(--accent); background: var(--panel-2); }
thead th.sorted { color: var(--accent); }
thead th .arr { margin-left: 3px; opacity: 0.5; font-size: 9px; }
thead th.sorted .arr { opacity: 1; }
tbody tr {
border-bottom: 1px solid var(--border);
cursor: pointer; transition: 100ms;
}
tbody tr:hover { background: var(--panel-2); }
tbody tr:last-child { border-bottom: none; }
tbody td { padding: 10px 12px; }
tbody td.num { text-align: right; font-family: 'JetBrains Mono', monospace; font-size: 12px; }
tbody td.dim { color: var(--text-3); }
/* BADGES */
.bdg {
display: inline-flex; align-items: center; gap: 4px;
padding: 2px 7px; border-radius: 4px;
font-size: 9.5px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.5px;
white-space: nowrap;
}
.bdg::before { content: ''; width: 4px; height: 4px; border-radius: 50%; }
.bdg.ok { background: rgba(45,212,191,0.13); color: var(--ok); }
.bdg.ok::before { background: var(--ok); }
.bdg.warn { background: rgba(245,158,11,0.13); color: var(--warn); }
.bdg.warn::before { background: var(--warn); }
.bdg.crit { background: rgba(239,68,68,0.13); color: var(--crit); }
.bdg.crit::before { background: var(--crit); }
.bdg.info { background: rgba(59,130,196,0.13); color: var(--accent); }
.bdg.info::before { background: var(--accent); }
.bdg.gold { background: rgba(245,158,11,0.13); color: var(--gold); }
.bdg.gold::before { background: var(--gold); }
.bdg.muted { background: rgba(107,122,153,0.13); color: var(--text-3); }
.bdg.muted::before { background: var(--text-3); }
/* BARS */
.bar { display: flex; align-items: center; padding: 5px 0; gap: 10px; font-size: 11.5px; }
.bar .l { width: 36%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 500; }
.bar .t { flex: 1; height: 6px; background: var(--bg-3); border-radius: 100px; overflow: hidden; }
.bar .f { height: 100%; background: linear-gradient(90deg, var(--accent), var(--gold)); border-radius: 100px; transition: width 500ms; }
.bar .f.ok { background: linear-gradient(90deg, var(--ok), var(--cyan)); }
.bar .f.warn { background: linear-gradient(90deg, var(--warn), #FB923C); }
.bar .f.crit { background: linear-gradient(90deg, var(--crit), #DC2626); }
.bar .f.gold { background: linear-gradient(90deg, var(--gold), #E5C064); }
.bar .v { width: 90px; text-align: right; font-family: 'JetBrains Mono', monospace; font-size: 11px; font-weight: 500; }
/* DONUT */
.donut-w { display: flex; gap: 16px; align-items: center; }
.donut { width: 110px; height: 110px; position: relative; flex-shrink: 0; }
.donut svg { transform: rotate(-90deg); }
.donut-c { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); text-align: center; }
.donut-c .v { font-size: 20px; font-weight: 700; font-family: 'JetBrains Mono', monospace; line-height: 1; }
.donut-c .l { font-size: 9px; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.7px; margin-top: 3px; }
.lg { font-size: 11px; flex: 1; }
.lg .it { display: flex; align-items: center; gap: 7px; padding: 4px 0; color: var(--text-2); }
.lg .it .sw { width: 9px; height: 9px; border-radius: 2px; flex-shrink: 0; }
.lg .it .lname { flex: 1; }
.lg .it .lval { font-family: 'JetBrains Mono', monospace; color: var(--text); font-weight: 500; }
/* BANNER */
.ban {
padding: 10px 14px; border-radius: var(--r-sm);
margin-bottom: 12px; display: flex; gap: 10px;
align-items: center; font-size: 12.5px;
border: 1px solid;
}
.ban.crit { background: rgba(239,68,68,0.08); border-color: rgba(239,68,68,0.3); color: var(--crit); }
.ban.warn { background: rgba(245,158,11,0.08); border-color: rgba(245,158,11,0.3); color: var(--warn); }
.ban.info { background: rgba(59,130,196,0.08); border-color: rgba(59,130,196,0.3); color: var(--accent); }
.ban.ok { background: rgba(45,212,191,0.08); border-color: rgba(45,212,191,0.3); color: var(--ok); }
.empty { text-align: center; color: var(--text-3); padding: 40px 18px; font-size: 13px; }
.empty-i { font-size: 28px; margin-bottom: 8px; opacity: 0.5; }
.loader { color: var(--text-3); padding: 50px 20px; text-align: center; font-size: 13px; }
/* DRAWER */
.dr-bg {
position: fixed; inset: 0;
background: rgba(10,14,26,0.7);
backdrop-filter: blur(4px);
z-index: 90;
opacity: 0; pointer-events: none;
transition: 200ms;
}
.dr-bg.open { opacity: 1; pointer-events: auto; }
.drawer {
position: fixed; top: 0; right: 0;
width: min(680px, 100%); height: 100vh; height: 100dvh;
background: var(--bg-2); border-left: 1px solid var(--border);
overflow-y: auto;
transform: translateX(100%);
transition: transform 250ms cubic-bezier(0.4,0,0.2,1);
z-index: 100;
box-shadow: -16px 0 48px rgba(0,0,0,0.6);
}
.drawer.open { transform: translateX(0); }
.dr-h {
padding: 18px 22px; border-bottom: 1px solid var(--border);
display: flex; justify-content: space-between; align-items: flex-start; gap: 10px;
position: sticky; top: 0; background: var(--bg-2); z-index: 5;
}
.dr-h h3 { font-size: 17px; font-weight: 700; letter-spacing: -0.2px; }
.dr-h .bc { font-size: 9px; color: var(--text-3); text-transform: uppercase; letter-spacing: 1.2px; margin-bottom: 3px; font-weight: 700; }
.dr-x {
background: var(--panel); border: 1px solid var(--border); color: var(--text-3);
cursor: pointer; width: 30px; height: 30px; border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: 14px; flex-shrink: 0; transition: 150ms;
}
.dr-x:hover { color: var(--text); border-color: var(--border-2); }
.dr-b { padding: 18px 22px 40px; }
.dr-b dl { display: grid; grid-template-columns: 130px 1fr; gap: 8px 14px; }
.dr-b dt { color: var(--text-3); font-size: 10px; text-transform: uppercase; letter-spacing: 0.6px; font-weight: 700; padding-top: 1px; }
.dr-b dd { color: var(--text); font-size: 13px; word-break: break-word; }
.dr-b dd a { color: var(--accent); text-decoration: none; }
.dr-b h4 {
font-size: 10px; color: var(--text-3);
text-transform: uppercase; letter-spacing: 1.3px; font-weight: 700;
margin: 22px 0 10px; padding-bottom: 8px;
border-bottom: 1px solid var(--border);
}
.sub-tbl { width: 100%; font-size: 12px; }
.sub-tbl td { padding: 7px 10px; border-bottom: 1px solid var(--border); }
.sub-tbl thead th { padding: 8px 10px; }
.blur-tag {
display: inline-block; margin-left: 5px;
font-size: 9px; padding: 1px 5px; border-radius: 3px;
background: rgba(245,158,11,0.15); color: var(--warn);
text-transform: uppercase; letter-spacing: 0.4px; font-weight: 700;
}
/* MODAL */
.modal-bg {
position: fixed; inset: 0;
background: rgba(10,14,26,0.85);
backdrop-filter: blur(8px);
display: none;
align-items: center; justify-content: center;
z-index: 1000;
padding: 20px;
}
.modal-bg.show { display: flex; }
.modal {
background: var(--panel); border: 1px solid var(--border);
border-radius: var(--r);
padding: 22px;
width: 100%; max-width: 400px;
box-shadow: 0 20px 60px rgba(0,0,0,0.6);
}
.modal h3 { margin-bottom: 6px; font-size: 17px; }
.modal p { color: var(--text-3); font-size: 12px; margin-bottom: 14px; line-height: 1.55; }
.modal .inp { margin-bottom: 10px; }
.ma { display: flex; gap: 8px; }
.hint {
font-size: 11px; color: var(--text-3);
padding: 9px 11px; background: var(--bg-2); border-radius: 6px;
margin-top: 10px; line-height: 1.55; border: 1px solid var(--border);
}
.hint b { color: var(--gold); }
/* MOBILE NAV */
.mob-nav {
display: none;
position: fixed; bottom: 0; left: 0; right: 0;
background: var(--bg-2);
border-top: 1px solid var(--border);
padding: 6px 0 calc(6px + env(safe-area-inset-bottom));
z-index: 50;
backdrop-filter: blur(12px);
}
.mob-nav-grid { display: grid; grid-template-columns: repeat(5, 1fr); }
.mob-nav-i {
display: flex; flex-direction: column; align-items: center; gap: 3px;
padding: 6px 4px;
color: var(--text-3);
cursor: pointer;
transition: 150ms;
}
.mob-nav-i.active { color: var(--accent); }
.mob-nav-i .ico { width: 20px; height: 20px; }
.mob-nav-i span { font-size: 9.5px; font-weight: 600; letter-spacing: 0.2px; }
/* MOBILE DRAWER NAV */
.mob-drawer {
position: fixed; top: 0; left: -300px; width: 280px; height: 100vh; height: 100dvh;
background: var(--bg-2); border-right: 1px solid var(--border);
transition: left 250ms; z-index: 200;
overflow-y: auto;
display: flex; flex-direction: column;
}
.mob-drawer.open { left: 0; }
.ico-svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.footer { padding: 18px 20px; color: var(--text-3); font-size: 10px; text-align: center; border-top: 1px solid var(--border); margin-top: 24px; }
/* ===== RESPONSIVE ===== */
@media (max-width: 880px) {
.sidebar { display: none; }
.menu-btn { display: inline-flex; }
.g4 { grid-template-columns: repeat(2, 1fr); }
.g3 { grid-template-columns: repeat(2, 1fr); }
.g2 { grid-template-columns: 1fr; }
.topbar { padding: 11px 14px; }
.content { padding: 14px 14px 90px; }
.mob-nav { display: block; }
.stat-v { font-size: 22px; }
.stat-v.lg { font-size: 24px; }
.stat-v.sm { font-size: 16px; }
.card { padding: 12px 14px; }
.ct { font-size: 10.5px; }
.tb-title h2 { font-size: 16px; }
.tb-bc { font-size: 9px; }
.donut-w { flex-direction: column; align-items: stretch; }
.donut { margin: 0 auto; }
.dr-b dl { grid-template-columns: 110px 1fr; }
.modal-bg { align-items: flex-end; padding: 0; }
.modal { max-width: 100%; border-radius: var(--r) var(--r) 0 0; padding: 22px; padding-bottom: calc(22px + env(safe-area-inset-bottom)); }
}
@media (max-width: 460px) {
.g4 { grid-template-columns: 1fr 1fr; }
.g3 { grid-template-columns: 1fr; }
.stat-v { font-size: 20px; }
.topbar { padding: 10px 12px; }
.content { padding: 12px 12px 90px; }
.fgrid { grid-template-columns: 1fr; }
table { font-size: 11.5px; }
tbody td, thead th { padding: 8px 10px; }
}
.klub-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 14px; }
.klub-card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 16px; cursor: pointer; transition: 200ms; }
.klub-card:hover { transform: translateY(-2px); border-color: var(--accent); box-shadow: 0 4px 16px rgba(0,0,0,0.3); }
.klub-card.gold-border { border-color: var(--gold); }
.klub-card-head { display: flex; gap: 12px; margin-bottom: 12px; }
.klub-logo { width: 44px; height: 44px; border-radius: 9px; background: linear-gradient(135deg, var(--accent), var(--gold)); display: flex; align-items: center; justify-content: center; font-weight: 700; color: var(--bg); font-size: 14px; flex-shrink: 0; }
.klub-info { flex: 1; min-width: 0; }
.klub-name { font-size: 14px; font-weight: 600; line-height: 1.3; }
.klub-savez { font-size: 11px; color: var(--text-dim); }
.klub-badges { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 12px; }
.klub-stats-mini { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; padding-top: 10px; border-top: 1px solid var(--border); }
.klub-stat-mini { text-align: center; }
.klub-stat-mini .v { font-family: 'JetBrains Mono', monospace; font-size: 16px; font-weight: 600; }
.klub-stat-mini .l { font-size: 9px; color: var(--text-dim); text-transform: uppercase; margin-top: 3px; }
.clan-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; }
.clan-card { background: var(--bg-3); border: 1px solid var(--border); border-radius: 8px; padding: 11px; }
.clan-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.clan-avatar { width: 32px; height: 32px; border-radius: 50%; background: var(--panel-2); display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 12px; color: var(--accent); }
.clan-name-x { flex: 1; min-width: 0; }
.clan-name-x .nm { font-size: 12.5px; font-weight: 600; }
.clan-name-x .pos { font-size: 10px; color: var(--text-dim); }
.clan-flags { display: flex; gap: 4px; flex-wrap: wrap; }
.clan-flag { padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 700; }
.drawer-stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 16px; }
.drawer-stat { background: var(--bg-3); border: 1px solid var(--border); border-radius: 7px; padding: 10px; text-align: center; }
.drawer-stat .v { font-family: 'JetBrains Mono', monospace; font-size: 18px; font-weight: 600; }
.drawer-stat .l { font-size: 9px; color: var(--text-dim); text-transform: uppercase; margin-top: 4px; }
.drawer-stat.ok .v { color: var(--ok); }
.drawer-stat.warn .v { color: var(--warn); }
.drawer-stat.crit .v { color: var(--crit); }
.drawer-stat.accent .v { color: var(--accent); }
.view-toggle { display: inline-flex; gap: 3px; background: var(--bg-2); padding: 3px; border-radius: 7px; border: 1px solid var(--border); }
.view-toggle button { background: none; border: none; color: var(--text-dim); padding: 5px 10px; font-size: 11px; font-weight: 600; cursor: pointer; border-radius: 5px; font-family: inherit; }
.view-toggle button.active { background: var(--accent); color: white; }
@media (max-width: 700px) {
.klub-grid { grid-template-columns: 1fr; }
.clan-list { grid-template-columns: 1fr; }
.drawer-stats-grid { grid-template-columns: repeat(2, 1fr); }
}
.ekosustav-grid { grid-template-columns: 1fr !important; gap: 12px !important; }
.ekosustav-coverage-row { font-size: 11px !important; }
.ekosustav-coverage-row > div:first-child { width: 100px !important; }
}
/* ===== TOPBAR — 2-row grid layout, mobile-first ===== */
.topbar { display: flex !important; flex-direction: column; gap: 8px; padding: 10px 14px; min-width: 0; }
.tb-row { display: flex; align-items: center; gap: 10px; min-width: 0; width: 100%; }
.tb-row-1 { gap: 12px; }
.tb-row-2 { padding: 0; }
.tb-title { flex: 1; min-width: 0; overflow: hidden; }
.tb-title h2 { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tb-meta { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--text-dim); white-space: nowrap; flex-shrink: 0; }
.tb-search {
display: flex; align-items: center; gap: 6px; flex: 1; width: 100%;
background: rgba(255,255,255,0.05); border: 1px solid var(--border);
border-radius: 8px; padding: 0 10px; height: 36px; position: relative;
transition: border-color 150ms;
}
.tb-search:focus-within { border-color: var(--accent); background: rgba(59,130,196,0.08); }
.tb-search input {
flex: 1; min-width: 0; background: transparent; border: 0;
color: var(--text); font-size: 13px; outline: none; padding: 0 4px; height: 100%;
}
.tb-search input::placeholder { color: var(--text-dim); }
.topbar-go {
background: var(--accent); color: white; border: 0; border-radius: 5px;
width: 28px; height: 26px; font-size: 14px; cursor: pointer; line-height: 1;
flex-shrink: 0;
}
.topbar-go:hover { opacity: 0.85; }
.top-search-suggest {
position: absolute; top: calc(100% + 4px); left: 0; right: 0;
background: var(--panel); border: 1px solid var(--border);
border-radius: 8px; max-height: 360px; overflow-y: auto;
z-index: 200; display: none; box-shadow: 0 8px 24px rgba(0,0,0,0.4);
}
.top-search-suggest.show { display: block; }
.tss-item { padding: 10px 12px; cursor: pointer; border-bottom: 1px solid var(--border); font-size: 13px; line-height: 1.4; }
.tss-item:last-child { border-bottom: 0; }
.tss-item:hover { background: rgba(59,130,196,0.08); }
.tss-tip {
display: inline-block; font-size: 9px; color: var(--accent);
text-transform: uppercase; letter-spacing: 0.5px; margin-right: 8px;
padding: 2px 5px; background: rgba(59,130,196,0.1); border-radius: 3px;
}
/* Desktop: single row layout */
@media (min-width: 881px) {
.topbar { flex-direction: row; align-items: center; gap: 14px; padding: 11px 18px; }
.tb-row-1 { flex: 0 0 auto; gap: 12px; flex: 1; max-width: 50%; }
.tb-row-2 { flex: 1; max-width: 480px; padding: 0; }
.tb-search { max-width: 480px; }
}
/* Mobile: row-2 search full width below row-1 */
@media (max-width: 880px) {
.tb-meta { font-size: 10px; }
.tb-time { display: none; }
.tb-search { height: 34px; }
.tb-search input { font-size: 13px; }
.ekosustav-grid { grid-template-columns: 1fr !important; gap: 14px !important; }
.ekosustav-coverage-row { font-size: 11px !important; }
.ekosustav-coverage-row > div:first-child { width: 110px !important; flex-shrink: 0; }
}
/* ===== V6 PRO FORM (Navision/SAP-style) ===== */
.v6-form { background:#1a1a1a; border:1px solid #2a2a2a; border-radius:6px; overflow:hidden; }
.v6-fh { background:linear-gradient(180deg,#2a3a52 0%,#1e2a3e 100%); border-bottom:1px solid #3a4a6a; padding:10px 16px; display:flex; justify-content:space-between; align-items:center; }
.v6-fh h3 { margin:0; font-size:14px; color:#fff; font-weight:600; }
.v6-fs { padding:12px 16px; border-bottom:1px solid #2a2a2a; }
.v6-fs-t { font-size:11px; color:#5e72e4; text-transform:uppercase; letter-spacing:0.5px; margin-bottom:8px; font-weight:600; }
.v6-g2 { display:grid; grid-template-columns:repeat(2,1fr); gap:8px 16px; }
.v6-g3 { display:grid; grid-template-columns:repeat(3,1fr); gap:8px 14px; }
.v6-g4 { display:grid; grid-template-columns:repeat(4,1fr); gap:8px 12px; }
@media (max-width:700px) { .v6-g2,.v6-g3,.v6-g4 { grid-template-columns:1fr; } }
.v6-fld { display:flex; flex-direction:column; }
.v6-w2 { grid-column:span 2; }
.v6-fld-w { grid-column:1/-1; }
.v6-lbl { font-size:11px; color:#98a8b8; margin-bottom:3px; font-weight:500; }
.v6-lbl.req::after { content:' *'; color:#e74c3c; }
.v6-inp { background:#0f1620; border:1px solid #2a3a4a; color:#e6e8ec; padding:6px 8px; font-size:13px; border-radius:3px; outline:none; font-family:inherit; }
.v6-inp:focus { border-color:#5e72e4; }
.v6-inp[readonly] { background:#1a242e; color:#788798; }
.v6-num { text-align:right; font-family:Consolas,monospace; }
.v6-calc { background:#1a2a1f !important; color:#4caf50 !important; font-weight:600; }
.v6-tot { background:linear-gradient(180deg,#1a2a1f 0%,#1e2a24 100%); padding:10px 16px; border-top:2px solid #4caf50; display:flex; justify-content:flex-end; gap:24px; }
.v6-tot-i { text-align:right; }
.v6-tot-i .v6-lbl { font-size:10px; }
.v6-tot-i .v6-val { font-size:18px; font-weight:700; color:#4caf50; font-family:Consolas,monospace; }
.v6-ac { position:relative; }
.v6-ac-s { position:absolute; top:100%; left:0; right:0; background:#0f1620; border:1px solid #5e72e4; border-top:none; max-height:200px; overflow-y:auto; z-index:100; display:none; }
.v6-ac-s.show { display:block; }
.v6-ac-s div { padding:6px 10px; cursor:pointer; font-size:13px; border-bottom:1px solid #2a2a2a; }
.v6-ac-s div:hover { background:#2a3a52; color:#fff; }
.v6-pill { display:inline-block; padding:2px 6px; background:#1a3a52; color:#5e72e4; font-size:10px; border-radius:3px; margin-left:6px; }
.v6-att-z { border:2px dashed #3a4a6a; border-radius:4px; padding:14px; text-align:center; cursor:pointer; background:#0f1620; }
.v6-att-z:hover { border-color:#5e72e4; }
.v6-att-l { margin-top:8px; display:flex; flex-direction:column; gap:4px; }
.v6-att-i { display:flex; align-items:center; gap:8px; padding:6px 10px; background:#1a2a3a; border-radius:3px; font-size:12px; }
.v6-att-i .v6-tag { background:#2a5e3a; color:#fff; padding:1px 6px; border-radius:2px; font-size:10px; }
.v6-att-i .v6-amt { margin-left:auto; font-family:Consolas,monospace; color:#4caf50; }
/* ===== V6.2 VOICE INPUT + CHATBOT ===== */
.v6-mic-btn {
background: #2a3a52;
border: 1px solid #3a4a6a;
color: #e6e8ec;
padding: 0 12px;
border-radius: 3px;
cursor: pointer;
font-size: 16px;
height: 100%;
transition: all 0.2s;
}
.v6-mic-btn:hover { background: #5e72e4; }
.v6-mic-btn.recording {
background: #c0392b;
border-color: #e74c3c;
animation: v6pulse 1.2s infinite;
}
@keyframes v6pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(231,76,60,0.7); }
50% { box-shadow: 0 0 0 8px rgba(231,76,60,0); }
}
.v6-chat-thread {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
max-height: 60vh;
overflow-y: auto;
background: #0f1620;
border-radius: 6px;
margin-bottom: 12px;
}
.v6-chat-msg {
padding: 10px 14px;
border-radius: 12px;
max-width: 85%;
font-size: 13px;
line-height: 1.5;
word-wrap: break-word;
}
.v6-chat-msg.user {
background: #5e72e4;
color: #fff;
align-self: flex-end;
border-bottom-right-radius: 3px;
}
.v6-chat-msg.bot {
background: #1a2a3a;
color: #e6e8ec;
align-self: flex-start;
border-bottom-left-radius: 3px;
white-space: pre-wrap;
}
.v6-chat-msg.bot .v6-msg-meta {
font-size: 10px;
color: #788798;
margin-bottom: 4px;
}
.v6-chat-msg .v6-src-link {
display: inline-block;
background: #2a5e3a;
color: #fff;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
margin: 2px 4px 2px 0;
cursor: pointer;
text-decoration: none;
}
.v6-chat-msg .v6-src-link:hover { background: #3a7e4a; }
.v6-chat-typing {
display: inline-block;
padding: 8px 14px;
background: #1a2a3a;
border-radius: 12px;
border-bottom-left-radius: 3px;
}
.v6-chat-typing span {
display: inline-block;
width: 8px;
height: 8px;
margin: 0 2px;
background: #5e72e4;
border-radius: 50%;
animation: v6typing 1.4s infinite;
}
.v6-chat-typing span:nth-child(2) { animation-delay: 0.2s; }
.v6-chat-typing span:nth-child(3) { animation-delay: 0.4s; }
@keyframes v6typing {
0%, 60%, 100% { opacity: 0.3; transform: translateY(0); }
30% { opacity: 1; transform: translateY(-4px); }
}
.v6-input-row {
display: flex;
gap: 8px;
align-items: stretch;
}
.v6-input-row .inp { flex: 1; }
</style>
<style id="ri-upgrade-css">
.ri-modal { position: fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.7); z-index:9999; display:flex; align-items:flex-start; justify-content:center; padding:30px 20px; overflow-y:auto; }
.ri-modal-box { background:var(--bg); border:1px solid var(--border); border-radius:8px; max-width:1000px; width:100%; box-shadow:0 8px 40px rgba(0,0,0,0.5);
max-height: 90vh;
display: flex;
flex-direction: column;
}
.ri-modal-h { display:flex; justify-content:space-between; align-items:center; padding:14px 18px; border-bottom:1px solid var(--border); background:var(--bg2); border-radius:8px 8px 0 0; }
.ri-modal-body { padding:14px; max-height:75vh; overflow-y:auto; }
.ri-icon-btn { background:none; border:none; color:var(--text2); cursor:pointer; padding:6px; border-radius:4px; display:inline-flex; align-items:center; justify-content:center; }
.ri-icon-btn:hover { background:var(--bg3); color:var(--text); }
.ri-icon-btn-sm { background:none; border:none; color:var(--text3); cursor:pointer; padding:4px; border-radius:3px; display:inline-flex; }
.ri-icon-btn-sm:hover { background:var(--bg3); color:var(--accent); }
.ri-badge { display:inline-block; padding:2px 8px; border-radius:3px; font-size:10px; font-weight:600; }
.ri-badge-gold { background:rgba(245,158,11,0.15); color:#f59e0b; border:1px solid rgba(245,158,11,0.3); }
.ri-badge-blue { background:rgba(59,130,246,0.15); color:#3b82f6; border:1px solid rgba(59,130,246,0.3); }
table.ri-sortable th.ri-sort { user-select:none; cursor:pointer; }
table.ri-sortable th.ri-sort:hover { color:var(--accent); }
.ri-sort.num { text-align:right; }
table.ri-sortable td.num, table.ri-sortable th.num { text-align:right; }
</style>
<style id="ri-actions-css">
.ri-actions { display:flex; gap:4px; align-items:center; }
.ri-actions .ri-icon-btn-sm { padding:5px; border:1px solid var(--border); border-radius:4px; background:var(--bg2); color:var(--text2); transition:all 0.15s; }
.ri-actions .ri-icon-btn-sm:hover { color:var(--accent); border-color:var(--accent); background:var(--bg3); }
</style>
</head>
<body>
<div class="app">
<!-- DESKTOP SIDEBAR -->
<aside class="sidebar">
<div class="sb-head">
<div class="brand">
<div class="brand-mark">PG</div>
<div class="brand-text">
<h1>PGŽ Sport</h1>
<p>Civic Intelligence OS</p>
</div>
</div>
<div id="role-pill" class="role-pill viewer" onclick="showLogin()">VIEWER</div>
</div>
<nav class="nav" id="nav-desktop"></nav>
<div class="sb-foot">
<div><b style="color:var(--gold)">Ri.NET</b> · DABI Digital B.V.</div>
<div>Damir Radulić · 2026</div>
</div>
</aside>
<!-- MAIN -->
<main class="main">
<div id="topbar"></div>
<div class="content"><div class="inner" id="content"><div class="loader">Učitavanje…</div></div></div>
</main>
</div>
<!-- MOBILE BOTTOM NAV -->
<nav class="mob-nav" id="mob-nav"></nav>
<!-- MOBILE SIDE DRAWER -->
<div class="dr-bg" id="mob-drawer-bg" onclick="toggleMobDrawer(false)"></div>
<aside class="mob-drawer" id="mob-drawer">
<div class="sb-head">
<div class="brand">
<div class="brand-mark">PG</div>
<div class="brand-text"><h1>PGŽ Sport</h1><p>Civic Intelligence OS</p></div>
</div>
<div id="role-pill-mob" class="role-pill viewer" onclick="showLogin()">VIEWER</div>
</div>
<nav class="nav" id="nav-mob"></nav>
<div class="sb-foot">
<div><b style="color:var(--gold)">Ri.NET</b> · DABI Digital</div>
<div>Damir Radulić</div>
</div>
</aside>
<!-- DETAIL DRAWER -->
<div class="dr-bg" id="drawer-bg" onclick="closeDrawer()"></div>
<div class="drawer" id="drawer"><div id="drawer-content"></div></div>
<!-- LOGIN MODAL — v2 (email+password + admin tab) -->
<div class="modal-bg" id="login-modal">
<div class="modal" style="min-width:340px;max-width:420px">
<div style="display:flex;gap:6px;margin-bottom:14px;border-bottom:1px solid var(--border);padding-bottom:10px">
<button id="loginTabUser" class="btn" style="flex:1;font-size:12px" onclick="loginSwitchTab('user')">👤 Korisnik</button>
<button id="loginTabAdmin" class="btn sec" style="flex:1;font-size:12px" onclick="loginSwitchTab('admin')">🔓 Admin token</button>
</div>
<div id="loginPanelUser">
<h3 style="margin-bottom:6px">Prijava korisnika</h3>
<p class="muted" style="margin-bottom:12px">Email i lozinka. Default lozinka novim korisnicima: <span class="mono">PgzSport2026!</span> (mora se promijeniti)</p>
<input class="inp" id="loginEmail" type="email" placeholder="email@pgz.hr" autocomplete="username" style="margin-bottom:8px;width:100%">
<input class="inp" id="loginPwd" type="password" placeholder="Lozinka" autocomplete="current-password" style="margin-bottom:12px;width:100%" onkeydown="if(event.key==='Enter')doUserLogin()">
<div id="loginError" style="display:none;color:var(--red);font-size:11px;margin-bottom:8px;padding:6px 10px;background:rgba(239,68,68,0.08);border:1px solid rgba(239,68,68,0.3);border-radius:4px"></div>
<div class="ma">
<button class="btn" style="flex:1" onclick="doUserLogin()">Prijavi se</button>
<button class="btn sec" onclick="closeLogin()">Odustani</button>
</div>
</div>
<div id="loginPanelAdmin" style="display:none">
<h3 style="margin-bottom:6px">Admin token</h3>
<p class="muted" style="margin-bottom:12px">Za PII unmask (OIB, IBAN, telefon). <b>GDPR čl. 5 i 32.</b></p>
<input class="inp mono" id="token-input" placeholder="Admin token..." autocomplete="off" style="margin-bottom:12px;width:100%">
<div class="ma">
<button class="btn" style="flex:1" onclick="doLogin()">Aktiviraj</button>
<button class="btn sec" onclick="closeLogin()">Odustani</button>
</div>
<div class="hint" style="margin-top:10px"><b>Demo:</b> <span class="mono">admin-pgz-2026</span></div>
</div>
</div>
</div>
<!-- MUST CHANGE PASSWORD MODAL -->
<div class="modal-bg" id="pwd-change-modal">
<div class="modal" style="min-width:340px;max-width:420px">
<h3>🔒 Promjena lozinke</h3>
<p class="muted" style="margin-bottom:12px">Vaša početna lozinka mora se promijeniti prije pristupa sustavu.</p>
<input class="inp" id="pwdNew1" type="password" placeholder="Nova lozinka (min 8 znakova)" autocomplete="new-password" style="margin-bottom:8px;width:100%">
<input class="inp" id="pwdNew2" type="password" placeholder="Potvrdi novu lozinku" autocomplete="new-password" style="margin-bottom:12px;width:100%" onkeydown="if(event.key==='Enter')doPwdChange()">
<div id="pwdError" style="display:none;color:var(--red);font-size:11px;margin-bottom:8px"></div>
<div class="ma">
<button class="btn" style="flex:1" onclick="doPwdChange()">Promijeni i nastavi</button>
<button class="btn sec" onclick="doLogout()">Odjavi se</button>
</div>
</div>
</div>
<script>
// Sprint3: image proxy for CORS-blocked external slike
function imgProxy(url) {
if (!url) return '';
if (url.startsWith('/') || url.startsWith('data:')) return url;
if (url.includes('/api/v2/img-proxy')) return url;
return '/sport/api/v2/img-proxy?u=' + encodeURIComponent(url);
}
const API = '/sport';
let state = { sort:{}, page:'dashboard', token: localStorage.getItem('pgz_token')||'', filters:{}, isAdmin:false };
const fmt = n => n==null||n===''?'':Number(n).toLocaleString('hr-HR',{maximumFractionDigits:0});
const fmtEur = n => n==null||n===''?'':Number(n).toLocaleString('hr-HR',{style:'currency',currency:'EUR',maximumFractionDigits:0});
const fmtDate = d => d?new Date(d).toLocaleDateString('hr-HR'):'';
const debounce = (fn,ms=300) => { let t; return (...a) => { clearTimeout(t); t=setTimeout(()=>fn(...a),ms); }; };
async function api(path, opts={}) {
const headers = { 'Content-Type':'application/json' };
if (state.token) headers['Authorization'] = 'Bearer '+state.token;
const res = await fetch(API+path, {...opts, headers:{...headers, ...(opts.headers||{})}});
if (!res.ok) throw new Error(`API ${res.status}`);
return res.json();
}
// === NAV CONFIG ===
const NAV = [
{ sec:'Pregled', exp:true, items:[
{ id:'dashboard', label:'Dashboard', mlabel:'Home', badge:false, svg:'<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>' },
{ id:'alertovi', label:'Alertovi', mlabel:'Alert', badge:true, svg:'<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>' },
{ id:'statistika', label:'Statistika', mlabel:'Stat', badge:false, svg:'<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>' }
]},
{ sec:'Registri', exp:true, items:[
{ id:'savezi', label:'Savezi', mlabel:'Sav', badge:false, svg:'<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>' },
{ id:'klubovi', label:'Klubovi', mlabel:'Klu', badge:false, svg:'<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>' },
{ id:'clanovi', label:'Clanovi', mlabel:'Clan', badge:false, svg:'<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>' },
{ id:'natjecanja', label:'Natjecanja', mlabel:'Nat', badge:false, svg:'<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>' },
{ id:'objekti', label:'Sportski objekti', mlabel:'Obj', badge:false, svg:'<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><line x1="3" y1="9" x2="21" y2="9"/>' },
{ id:'hns', label:'HNS Natjecanja', mlabel:'HNS', badge:false, svg:'<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15 15 0 0 1 4 10 15 15 0 0 1-4 10z"/>' }
]},
{ sec:'Financije', exp:false, items:[
{ id:'proracun', label:'Proracun', mlabel:'Bud', badge:false, svg:'<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>' },
{ id:'potpore', label:'Potpore', mlabel:'Pot', badge:false, svg:'<polyline points="20 12 20 22 4 22 4 12"/><rect x="2" y="7" width="20" height="5"/><line x1="12" y1="22" x2="12" y2="7"/>' },
{ id:'rno', label:'Registar NPO', mlabel:'NPO', badge:false, svg:'<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>' },
{ id:'clanarine', label:'Clanarine', mlabel:'Clar', badge:false, svg:'<rect x="1" y="4" width="22" height="16" rx="2" ry="2"/><line x1="1" y1="10" x2="23" y2="10"/>' },
{ id:'invoices', label:'Fakture', mlabel:'Fak', badge:false, svg:'<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>' },
{ id:'expenses', label:'Troskovi', mlabel:'Tro', badge:false, svg:'<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>' }
]},
{ sec:'Zdravlje & Pravo', exp:false, items:[
{ id:'lijecnicki', label:'Lijecnicki', mlabel:'Med', badge:true, svg:'<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>' },
{ id:'zzjz', label:'ZZJZ', mlabel:'ZZJZ', badge:false, svg:'<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>' },
{ id:'pravnik', label:'AI Pravnik', mlabel:'Prav', badge:false, svg:'<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>' },
{ id:'manifestacije',label:'Manifestacije', mlabel:'Man', badge:false, svg:'<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>' }
]},
{ sec:'AI & Analitika', exp:false, items:[
{ id:'ask', label:'AI Chat', mlabel:'AI', badge:false, svg:'<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>' },
{ id:'godisnjaci', label:'Godisnjaci AI', mlabel:'God', badge:false, svg:'<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>' },
{ id:'dms', label:'DMS Dokumenti', mlabel:'DMS', badge:false, svg:'<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>' },
{ id:'dataQuality', label:'Kvaliteta podataka', mlabel:'KP', badge:false, svg:'<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>' },
{ id:'graf', label:'Grafovi', mlabel:'Graf', badge:false, svg:'<rect x="2" y="2" width="4" height="20"/><rect x="10" y="8" width="4" height="14"/><rect x="18" y="5" width="4" height="17"/>' },
{ id:'analytics', label:'Analitika', mlabel:'Ana', badge:false, svg:'<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>' },
{ id:'search', label:'Napredna pret.', mlabel:'Srch', badge:false, svg:'<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>' }
]},
{ sec:'Admin', exp:false, items:[
{ id:'admin', label:'Administracija', mlabel:'Adm', badge:false, svg:'<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>' }
]}
];
function buildNavs() {
const dEl = document.getElementById('nav-desktop');
const mEl = document.getElementById('nav-mob');
if (!dEl) return;
let dHtml = '';
const mItems = [];
NAV.forEach((s, si) => {
const isExp = s.exp !== false;
dHtml += `<div><div class="nav-sec" style="cursor:pointer;display:flex;align-items:center;justify-content:space-between" onclick="toggleNavSec(${si})"><span>${s.sec}</span><span id="nc${si}" style="font-size:9px;opacity:.4;transition:transform .2s;display:inline-block;transform:${isExp?'rotate(90deg)':'rotate(0deg)'}">&rsaquo;</span></div><div id="ns${si}" style="display:${isExp?'block':'none'}">`;
s.items.forEach(it => {
const badge = it.badge ? `<span class="b" id="b-${it.id}" style="display:none">0</span>` : '';
dHtml += `<div class="nav-i" data-page="${it.id}" onclick="goto('${it.id}')"><svg class="ico ico-svg" viewBox="0 0 24 24">${it.svg}</svg><span>${it.label}</span>${badge}</div>`;
mItems.push(it);
});
dHtml += `</div></div>`;
});
dEl.innerHTML = dHtml;
// Mobile: top 5 most used
const mob5 = ['dashboard','klubovi','clanovi','lijecnicki','alertovi'];
if (mEl) mEl.innerHTML = mItems.filter(i => mob5.includes(i.id)).slice(0,5).map(it =>
`<div class="nav-i" data-page="${it.id}" onclick="goto('${it.id}')"><svg class="ico ico-svg" viewBox="0 0 24 24">${it.svg}</svg><span>${it.mlabel}</span></div>`
).join('');
}
function toggleNavSec(si) {
const el = document.getElementById('ns'+si);
const ch = document.getElementById('nc'+si);
if (!el) return;
const vis = el.style.display !== 'none';
el.style.display = vis ? 'none' : 'block';
if (ch) ch.style.transform = vis ? 'rotate(0deg)' : 'rotate(90deg)';
}
function goto(page) {
// Update URL hash for easy sharing/debugging
if (history.pushState) {
history.pushState(null, null, '#' + page);
} else {
window.location.hash = page;
}
state.page = page;
state.filters = {}; state.sort = {};
document.querySelectorAll('.nav-i').forEach(e => e.classList.toggle('active', e.dataset.page===page));
document.querySelectorAll('.mob-nav-i').forEach(e => e.classList.toggle('active', e.dataset.page===page));
toggleMobDrawer(false);
render();
}
function toggleMobDrawer(open) {
document.getElementById('mob-drawer').classList.toggle('open', open);
document.getElementById('mob-drawer-bg').classList.toggle('open', open);
}
function openDrawer(html) {
document.getElementById('drawer-content').innerHTML = html;
document.getElementById('drawer').classList.add('open');
document.getElementById('drawer-bg').classList.add('open');
}
function closeDrawer() {
document.getElementById('drawer').classList.remove('open');
document.getElementById('drawer-bg').classList.remove('open');
}
// ═══ AUTH v3 ═══
function loginSwitchTab(which) {
const u = document.getElementById('loginPanelUser'), a = document.getElementById('loginPanelAdmin');
const tu = document.getElementById('loginTabUser'), ta = document.getElementById('loginTabAdmin');
if (which === 'user') { u.style.display=''; a.style.display='none'; tu.classList.remove('sec'); ta.classList.add('sec'); }
else { u.style.display='none'; a.style.display=''; ta.classList.remove('sec'); tu.classList.add('sec'); }
}
function showLogin(tab) {
document.getElementById('login-modal').classList.add('show');
document.getElementById('loginError').style.display='none';
loginSwitchTab(tab || 'user');
setTimeout(()=>{ const el = document.getElementById('loginEmail'); if (el) el.focus(); }, 50);
if (window.google && window.google.accounts) {
const btn = document.getElementById('google-signin-btn');
if (btn && btn.children.length === 0) initGoogleSignIn();
}
}
function closeLogin() { document.getElementById('login-modal').classList.remove('show'); }
// Admin token (PII unmask, legacy)
function doLogin() {
const t = document.getElementById('token-input').value.trim();
state.token = t; localStorage.setItem('pgz_token', t);
closeLogin(); checkRole().then(()=>render());
}
// User login (v2 email+pwd) — primary
async function doUserLogin() {
const email = document.getElementById('loginEmail').value.trim();
const pwd = document.getElementById('loginPwd').value;
const err = document.getElementById('loginError');
err.style.display = 'none';
if (!email || !pwd) { err.textContent='Unesite email i lozinku.'; err.style.display='block'; return; }
try {
const r = await fetch('/sport/api/v2/auth/login', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({email, password: pwd})
});
const d = await r.json();
if (!r.ok) throw new Error(d.detail || 'Pogrešni podaci');
localStorage.setItem('rinet_v2_token', d.token);
localStorage.setItem('rinet_v2_user', JSON.stringify(d.user || {}));
state.v2Token = d.token; state.v2User = d.user || {};
closeLogin();
// must_change_pwd flow
if (d.user && d.user.must_change_pwd) {
document.getElementById('pwd-change-modal').classList.add('show');
return;
}
await checkRole();
render();
} catch(e) {
err.textContent = e.message || 'Greška pri prijavi';
err.style.display = 'block';
}
}
async function doPwdChange() {
const p1 = document.getElementById('pwdNew1').value;
const p2 = document.getElementById('pwdNew2').value;
const err = document.getElementById('pwdError');
err.style.display='none';
if (p1.length < 8) { err.textContent='Lozinka mora imati barem 8 znakova.'; err.style.display='block'; return; }
if (p1 !== p2) { err.textContent='Lozinke se ne podudaraju.'; err.style.display='block'; return; }
try {
const r = await fetch('/sport/api/v2/auth/change-password', {
method:'POST', headers:{'Content-Type':'application/json','Authorization':'Bearer '+state.v2Token},
body: JSON.stringify({new_password: p1})
});
const d = await r.json();
if (!r.ok) throw new Error(d.detail || 'Greška');
document.getElementById('pwd-change-modal').classList.remove('show');
if (state.v2User) { state.v2User.must_change_pwd = false; localStorage.setItem('rinet_v2_user', JSON.stringify(state.v2User)); }
await checkRole();
render();
} catch(e) { err.textContent=e.message; err.style.display='block'; }
}
async function doLogout() {
try { await fetch('/sport/api/v2/auth/logout', {method:'POST', headers:{'Authorization':'Bearer '+(state.v2Token||'')}}); } catch(e){}
localStorage.removeItem('rinet_v2_token');
localStorage.removeItem('rinet_v2_user');
state.v2Token = ''; state.v2User = null;
document.getElementById('pwd-change-modal').classList.remove('show');
await checkRole();
render();
}
async function checkRole() {
// 1) Admin token (legacy PII unmask)
try {
const w = await api('/api/whoami');
state.isAdmin = ['super_admin','pgz_admin','pgz_user'].includes(w.user_type);
} catch(e) { state.isAdmin = false; }
// 2) v2 user session
state.v2Token = localStorage.getItem('rinet_v2_token') || '';
try { state.v2User = JSON.parse(localStorage.getItem('rinet_v2_user') || 'null'); } catch(e) { state.v2User = null; }
if (state.v2Token) {
try {
const r = await fetch('/sport/api/v2/auth/me', { headers: {'Authorization':'Bearer '+state.v2Token} });
if (r.ok) {
const me = await r.json();
state.v2User = me;
localStorage.setItem('rinet_v2_user', JSON.stringify(me));
} else if (r.status === 401) {
localStorage.removeItem('rinet_v2_token');
localStorage.removeItem('rinet_v2_user');
state.v2Token = ''; state.v2User = null;
}
} catch(e) {}
}
renderRolePill();
}
function renderRolePill() {
['role-pill', 'role-pill-mob'].forEach(id => {
const p = document.getElementById(id);
if (!p) return;
if (state.v2User && state.v2User.email) {
const u = state.v2User;
const ut = u.user_type || (u.roles && u.roles[0]) || 'user';
const name = u.ime ? (u.ime + (u.prezime ? ' ' + u.prezime[0] + '.' : '')) : (u.full_name || u.email);
p.className = 'role-pill admin';
p.innerHTML = '<span style="font-size:9px;opacity:0.7">' + ut.toUpperCase() + '</span><br><span style="font-size:10px">' + name + '</span>';
p.onclick = () => {
if (confirm('Odjaviti se?')) doLogout();
};
p.title = 'Klik za odjavu (' + u.email + ')';
} else if (state.isAdmin) {
p.className = 'role-pill admin';
p.textContent = 'ADMIN TOKEN';
p.onclick = () => { state.token = ''; localStorage.removeItem('pgz_token'); checkRole(); render(); };
} else {
p.className = 'role-pill viewer';
p.textContent = '🔐 PRIJAVA';
p.onclick = () => showLogin('user');
}
});
}
// Global 401 handler — redirect to login
window._origFetch = window.fetch;
window.fetch = async function(...args) {
const r = await window._origFetch.apply(this, args);
if (r.status === 401) {
const url = (args[0] || '').toString();
if (url.includes('/api/v2/') && !url.includes('/auth/')) {
// soft trigger: only show login if we're not already trying to log in
const m = document.getElementById('login-modal');
if (m && !m.classList.contains('show')) {
setTimeout(() => showLogin('user'), 100);
}
}
}
return r;
};
// State extension
state.v2Token = localStorage.getItem('rinet_v2_token') || '';
try { state.v2User = JSON.parse(localStorage.getItem('rinet_v2_user') || 'null'); } catch(e) { state.v2User = null; }
// API helper extension — auto-attach v2 bearer for /api/v2/*
const _origApi = api;
window.api = async function(path, opts={}) {
const headers = { 'Content-Type':'application/json' };
if (path.startsWith('/api/v2/') && state.v2Token) {
headers['Authorization'] = 'Bearer ' + state.v2Token;
} else if (state.token) {
headers['Authorization'] = 'Bearer ' + state.token;
}
const res = await fetch(API + path, {...opts, headers:{...headers, ...(opts.headers||{})}});
if (!res.ok) {
let detail = '';
try { detail = (await res.json()).detail || ''; } catch(e) {}
throw new Error('API ' + res.status + (detail ? ': ' + detail : ''));
}
return res.json();
};
function globalSearch(q) {
if (!q || q.length < 2) return;
state.page = 'search'; state.searchQ = q; render();
}
async function pageSearch() {
setTopbar('AI Search', 'Rezultati: "' + (state.searchQ || '') + '"');
// Add enrich button to top
setTimeout(() => {
const tb = document.querySelector('.topbar');
if (tb && !tb.querySelector('.ai-enrich-btn') && state.searchQ) {
const btn = document.createElement('button');
btn.className = 'btn primary ai-enrich-btn';
btn.style.cssText = 'margin-left:14px;display:inline-flex;align-items:center;gap:6px;padding:6px 12px';
btn.innerHTML = iconExternal() + ' AI obogati pretragu';
btn.onclick = () => showEnrichModal('search', null, state.searchQ, state.searchQ);
tb.appendChild(btn);
}
}, 100);
const c = document.getElementById('content');
if (!state.searchQ) {
c.innerHTML = '<div style="max-width:680px;margin:30px auto">'
+ '<div class="card" style="padding:24px">'
+ '<h3 style="margin:0 0 14px;color:var(--text)">AI Search</h3>'
+ '<p class="muted" style="margin-bottom:20px">Pretraži klubove, saveze, sportaše, manifestacije, pravilnike i dokumente PGŽ-a.</p>'
+ '<div style="display:flex;gap:8px;margin-bottom:14px"><input id="aiSearchInline" type="text" placeholder="npr. nogomet Rijeka, kategorizacija, sufinanciranje... ili 🎤 reci" style="flex:1;padding:14px 16px;font-size:15px;background:var(--bg-1);color:var(--text);border:1px solid var(--border-1);border-radius:8px;outline:none" onkeydown="if(event.key===\'Enter\'){state.searchQ=this.value;render()}" autofocus /><button class="v6-mic-btn" style="padding:0 16px;font-size:18px;border-radius:8px" onclick="v6VoiceStart(\'aiSearchInline\', this)" title="Glasovni unos (hr-HR)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></button></div>'
+ '<div style="display:flex;flex-wrap:wrap;gap:8px">'
+ ['nogomet','košarka','kategorizacija','pravilnik','financiranje','klub Rijeka','sportaš seniori','manifestacija 2026','medicinski pregled','natjecaji']
.map(q => '<span class="badge muted" style="cursor:pointer" onclick="state.searchQ=\''+q+'\';render()">' + q + '</span>').join('')
+ '</div>'
+ '<div style="margin-top:20px;padding-top:14px;border-top:1px solid var(--border-1);font-size:11px;color:var(--text-dim)">'
+ 'PGŽ-only po default · 220 saveza · 1622 klubova · 6915 dokumenata · 52k vektora'
+ '</div></div></div>';
setTimeout(()=>{const el=document.getElementById('aiSearchInline'); if(el)el.focus()}, 100);
return;
}
c.innerHTML = '<div class="loader">Pretraga BGE-M3...</div>';
try {
const tip = state.filters.tip || '';
const scope = state.filters.searchScope || 'pgz';
const d = await api('/api/search?q=' + encodeURIComponent(state.searchQ) + '&limit=20&scope=' + scope + (tip ? '&tip=' + tip : ''));
const tipBadge = { savez:'info', klub:'gold', clan:'muted', manifestacija:'warn', potpora:'ok', proracun:'crit', statistika:'info', dokument:'info', natjecanje:'gold', kategorija:'muted', zakon:'crit' };
var hdr = '<div class="filter-bar"><div class="filter-bar-title">Pretraga: <b>'+state.searchQ+'</b></div>'
+ '<div class="filter-grid" style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">'
+ '<input id="searchInput" class="inp" value="'+state.searchQ.replace(/"/g,'&quot;')+'" style="flex:1;min-width:200px" onkeydown="if(event.key===\'Enter\'){state.searchQ=this.value;render()}" />'
+ '<button class="v6-mic-btn" onclick="v6VoiceStart(\'searchInput\', this)" title="Glasovni unos (hr-HR)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></button>'
+ '<button class="btn primary" onclick="state.searchQ=document.getElementById(\'searchInput\').value;render()">Traži</button>'
+ '<button class="btn" onclick="state.searchQ=\'\';render()" title="Nova pretraga">Reset</button>'
+ '<select onchange="state.filters.searchScope=this.value;render()" class="inp" style="min-width:140px">'
+ '<option value="pgz"'+(scope==='pgz'?' selected':'')+'>Samo PGŽ</option>'
+ '<option value="all"'+(scope==='all'?' selected':'')+'>Sve (Hrvatska)</option>'
+ '<option value="national"'+(scope==='national'?' selected':'')+'>Samo nacional</option>'
+ '</select>'
+ '<select onchange="state.filters.tip=this.value;render()" class="inp" style="min-width:140px">'
+ '<option value="">Svi tipovi</option>'
+ '<option value="klub"'+(tip==='klub'?' selected':'')+'>Klubovi</option>'
+ '<option value="savez"'+(tip==='savez'?' selected':'')+'>Savezi</option>'
+ '<option value="dokument"'+(tip==='dokument'?' selected':'')+'>Dokumenti</option>'
+ '<option value="manifestacija"'+(tip==='manifestacija'?' selected':'')+'>Manifestacije</option>'
+ '<option value="natjecanje"'+(tip==='natjecanje'?' selected':'')+'>Natjecanja</option>'
+ '<option value="zakon"'+(tip==='zakon'?' selected':'')+'>Zakoni</option>'
+ '</select>'
+ '</div></div>'
+ '<div style="color:var(--text-dim);font-size:11px;margin-bottom:14px">'+d.count+' rezultata · scope: '+(d.scope||'pgz')+'</div>';
c.innerHTML = hdr + '<div class="grid" style="grid-template-columns:1fr;gap:8px">' +
d.results.map(function(r){
var url = r.url || (r.payload && (r.payload.source_url || r.payload.url)) || '';
var title = r.naziv || (r.payload && r.payload.title) || '(bez naslova)';
var docType = r.payload && r.payload.doc_type;
var sourceTag = r.payload && r.payload.source;
var publishDate = r.payload && r.payload.publish_date;
var relevance = r.relevance || '';
var click = '';
var hint = '';
if (url) { click = 'onclick="window.open(\''+url.replace(/\x27/g,'\\x27')+'\',\'_blank\')" style="cursor:pointer"'; hint = '<span class="pill" style="background:#2a5e3a;color:#fff">otvori dokument</span>'; }
else if (r.klub_id) { click = 'onclick="showKlub('+r.klub_id+')" style="cursor:pointer"'; hint = '<span class="pill ok">klub →</span>'; }
else if (r.savez_id) { click = 'onclick="showSavez('+r.savez_id+')" style="cursor:pointer"'; hint = '<span class="pill ok">savez →</span>'; }
var relB = relevance==='pgz' ? '<span class="pill" style="background:#1a4d3a;color:#27c79b">PGŽ</span>' :
relevance==='national_doc' ? '<span class="pill" style="background:#3a3a52">nacional</span>' : '';
return '<div class="card" '+click+'>'
+ '<div style="display:flex;justify-content:space-between;gap:10px;margin-bottom:6px;flex-wrap:wrap">'
+ '<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">'
+ '<span class="badge '+(tipBadge[r.tip]||'muted')+'">'+(r.tip||'?')+'</span>'
+ relB
+ (docType ? '<span class="pill muted">'+docType+'</span>' : '')
+ '<strong>'+title+'</strong>'
+ hint
+ '</div>'
+ '<span class="mono" style="font-size:10px;color:var(--text-dim)">'+(publishDate?publishDate.slice(0,10)+' · ':'')+(sourceTag||'')+' · '+(r.score*100).toFixed(0)+'%</span>'
+ '</div>'
+ '<div style="color:var(--text-2);font-size:12px;line-height:1.5">'+((r.tekst||'').slice(0,300))+((r.tekst||'').length>300?'…':'')+'</div>'
+ (url ? '<div style="margin-top:4px;font-size:11px;color:#5e72e4;word-break:break-all">'+(url.length>120?url.slice(0,120)+'…':url)+'</div>' : '')
+ '</div>';
}).join('') +
(d.count===0 ? '<div class="empty">Nema rezultata. Pokušaj drugi pojam ili promijeni scope.</div>' : '') +
'</div>';
} catch (e) { c.innerHTML = '<div class="banner crit">'+e.message+'</div>'; }
}
function setTopbar(bc, title, meta='') {
document.getElementById('topbar').innerHTML = `
<div class="topbar">
<div class="tb-row tb-row-1">
<button class="menu-btn" onclick="toggleMobDrawer(true)">
<svg class="ico-svg" viewBox="0 0 24 24" style="width:18px;height:18px"><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
<div class="tb-title">
<div class="tb-bc">${bc}</div>
<h2>${title}</h2>
</div>
<div class="tb-meta">${meta} <span class="live-dot"></span><span class="tb-time">${new Date().toLocaleTimeString('hr-HR',{hour:'2-digit',minute:'2-digit'})}</span></div>
</div>
<div class="tb-row tb-row-2">
<div class="tb-search">
<svg class="ico-svg" viewBox="0 0 24 24" style="width:14px;height:14px;color:var(--text-dim);flex-shrink:0"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.5" y2="16.5"/></svg>
<input type="search" id="topSearchInput" placeholder="Pretraži klubove, saveze, sportaše..." autocomplete="off"
oninput="topSearchType(this.value)" onkeydown="if(event.key==='Enter')topSearchGo(this.value)" />
<button class="v6-mic-btn" style="padding:0 10px;border-radius:0 8px 8px 0;height:auto" onclick="v6VoiceStart('topSearchInput', this)" title="Glasovni unos (hr-HR)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></button>
<button class="topbar-go" onclick="topSearchGo(document.getElementById('topSearchInput').value)" title="Pretraži">→</button>
</div>
</div>
</div>
`;
}
let _topSearchTimer = null;
function topSearchType(q) {
clearTimeout(_topSearchTimer);
if (!q || q.length < 2) {
const el = document.getElementById('topSearchSuggest');
if (el) el.classList.remove('show');
return;
}
_topSearchTimer = setTimeout(() => topSearchSuggestFetch(q), 250);
}
async function topSearchSuggestFetch(q) {
try {
const r = await fetch('/sport/api/search?q=' + encodeURIComponent(q) + '&limit=8');
const d = await r.json();
let el = document.getElementById('topSearchSuggest');
if (!el) {
el = document.createElement('div');
el.id = 'topSearchSuggest';
el.className = 'top-search-suggest';
const cont = document.querySelector('.tb-search');
if (cont) {
cont.style.position = 'relative';
cont.appendChild(el);
}
}
if (!d.results || d.results.length === 0) {
el.innerHTML = '<div class="tss-item dim">Nema rezultata za "' + q + '"</div>';
} else {
el.innerHTML = d.results.map(function(r) {
var p = r.payload || {};
var tip = r.tip || p.tip || '';
var naziv = r.naziv || p.naziv || p.title || '?';
var url = r.url || p.source_url || p.url || '';
var id = r.klub_id || r.savez_id || (p && (p.klub_id || p.savez_id)) || '';
var score = r.score ? r.score.toFixed(2) : '';
var onClick;
if (url) {
var safeUrl = url.replace(/\x27/g, '%27').replace(/"/g, '%22');
onClick = 'window.open(\x27' + safeUrl + '\x27, \x27_blank\x27)';
} else if (tip === 'klub' && id) onClick = 'showKlub(' + id + ')';
else if (tip === 'savez' && id) onClick = 'showSavez(' + id + ')';
else if (tip === 'savez') onClick = 'goto(\x27savezi\x27)';
else { var sq = q.replace(/\x27/g, ''); onClick = 'state.searchQ=\x27' + sq + '\x27; goto(\x27search\x27)'; }
var safeNaziv = (naziv || '?').replace(/</g,'&lt;').replace(/>/g,'&gt;');
if (safeNaziv.length > 60) safeNaziv = safeNaziv.slice(0,60) + '…';
return '<div class="tss-item" onclick="' + onClick + ';document.getElementById(\x27topSearchSuggest\x27).classList.remove(\x27show\x27)">' +
'<span class="tss-tip">' + tip + '</span>' +
'<span>' + safeNaziv + '</span>' +
(url ? '<span class="dim" style="font-size:10px;margin-left:6px">📄</span>' : '') +
(score ? '<span class="dim" style="float:right;font-size:10px">' + score + '</span>' : '') +
'</div>';
}).join('');
}
el.classList.add('show');
} catch (e) { console.error('topSearch err', e); }
}
function topSearchGo(q) {
if (!q) return;
state.searchQ = q;
const el = document.getElementById('topSearchSuggest');
if (el) el.classList.remove('show');
goto('search');
}
document.addEventListener('click', function(e) {
const search = document.querySelector('.tb-search');
if (search && !search.contains(e.target)) {
const sug = document.getElementById('topSearchSuggest');
if (sug) sug.classList.remove('show');
}
});
function tableHeader(cols, sortKey) {
return cols.map(c => {
const s = c.sort !== false;
const sorted = state.sort[sortKey] && state.sort[sortKey].col === c.key ? 'sorted' : '';
const arr = sorted ? (state.sort[sortKey].order==='asc'?'↑':'↓') : '↕';
return `<th class="${sorted}" ${s?`onclick="sortBy('${sortKey}','${c.key}')"`:''}>${c.label}${s?` <span class="arr">${arr}</span>`:''}</th>`;
}).join('');
}
function sortBy(sk, col) {
const s = state.sort[sk] || {col:'', order:'asc'};
if (s.col===col) s.order = s.order==='asc'?'desc':'asc';
else { s.col=col; s.order='asc'; }
state.sort[sk] = s; render();
}
function getSort(sk) { const s = state.sort[sk]; return s && s.col ? `&sort=${s.col}&order=${s.order}` : ''; }
function donut(values, labels, colors, totalDisplay, label) {
const sum = values.reduce((a,b)=>a+b,0) || 1;
const r = 46, c = 2*Math.PI*r;
let off = 0;
const segs = values.map((v,i) => {
const len = (v/sum)*c;
const seg = `<circle r="${r}" cx="55" cy="55" fill="transparent" stroke="${colors[i]}" stroke-width="12" stroke-dasharray="${len} ${c-len}" stroke-dashoffset="${-off}"/>`;
off += len; return seg;
}).join('');
return `<div class="donut"><svg width="110" height="110" viewBox="0 0 110 110">${segs}</svg>
<div class="donut-c"><div class="v">${totalDisplay}</div><div class="l">${label||''}</div></div></div>`;
}
function lineChart(series, labels, w=600, h=200, colors=['#4A9EFF','#D4A852','#A78BFA','#F472B6','#2DD4BF','#22D3EE','#F59E0B']) {
const pad = {l:50, r:14, t:12, b:28};
const iw = w-pad.l-pad.r, ih = h-pad.t-pad.b;
const all = series.flatMap(s=>s.data);
const max = Math.max(...all,1)*1.05, min = 0;
const xs = iw / Math.max(labels.length-1, 1);
const lines = series.map((s,si) => {
const pts = s.data.map((v,i) => `${pad.l+i*xs},${pad.t+ih-(v-min)/(max-min)*ih}`).join(' ');
return `<polyline fill="none" stroke="${colors[si%colors.length]}" stroke-width="2.5" points="${pts}" stroke-linejoin="round"/>` +
s.data.map((v,i) => `<circle cx="${pad.l+i*xs}" cy="${pad.t+ih-(v-min)/(max-min)*ih}" r="3" fill="${colors[si%colors.length]}" stroke="var(--bg)" stroke-width="1.5"/>`).join('');
}).join('');
const xax = labels.map((l,i) => `<text x="${pad.l+i*xs}" y="${h-10}" fill="var(--text-3)" font-size="10" font-family="JetBrains Mono" text-anchor="middle">${l}</text>`).join('');
const yt = [0,0.25,0.5,0.75,1].map(p => {
const y = pad.t+ih*(1-p);
return `<line x1="${pad.l}" y1="${y}" x2="${w-pad.r}" y2="${y}" stroke="var(--border)" stroke-dasharray="3,3" opacity="0.5"/>
<text x="${pad.l-6}" y="${y+3}" fill="var(--text-3)" font-size="9" font-family="JetBrains Mono" text-anchor="end">${fmt(min+p*(max-min))}</text>`;
}).join('');
const lg = series.map((s,i) => `<div class="it"><div class="sw" style="background:${colors[i%colors.length]}"></div><span class="lname">${s.label}</span></div>`).join('');
return `<svg viewBox="0 0 ${w} ${h}" style="width:100%;height:auto" preserveAspectRatio="xMidYMid meet">${yt}${lines}${xax}</svg>
<div class="lg" style="display:flex;gap:14px;flex-wrap:wrap;margin-top:8px">${lg}</div>`;
}
function barChart(items, getLbl, getVal, fillClass='', formatter=fmt) {
const max = Math.max(...items.map(getVal),1);
return items.map(it => `<div class="bar">
<div class="l" title="${getLbl(it)}">${getLbl(it)}</div>
<div class="t"><div class="f ${fillClass}" style="width:${(getVal(it)/max*100).toFixed(1)}%"></div></div>
<div class="v">${formatter(getVal(it))}</div></div>`).join('');
}
// ========== PAGES ==========
async function fetchEkosustav() {
try {
const e = await api('/api/dashboard/ekosustav');
const c = e.coverage || {};
const rows = [
['🆔 OIB', c.oib_pct, e.s_oib, e.klubova_total],
['👤 Predsjednik', c.predsjednik_pct, e.s_predsjednik, e.klubova_total],
['🎯 Ciljevi', c.ciljevi_pct, e.s_ciljevi, e.klubova_total],
['📋 Djelatnosti', c.opis_pct, e.s_opis, e.klubova_total],
['🏛️ Savez', c.savez_pct, e.s_savez, e.klubova_total],
['📍 Sjedište', c.sjediste_pct, e.s_sjediste, e.klubova_total],
['👥 Tajnik', c.tajnik_pct, e.s_tajnik, e.klubova_total],
['📧 Email', c.email_pct, e.s_email, e.klubova_total],
];
const barColor = (pct) => pct >= 80 ? 'var(--ok)' : (pct >= 50 ? 'var(--gold)' : 'var(--accent)');
const coverageHTML = rows.map(([label, pct, count, total]) => `
<div class="ekosustav-coverage-row" style="display:flex;align-items:center;gap:10px;font-size:12px;padding:6px 0;border-bottom:1px solid var(--border)">
<div style="width:120px;color:var(--text)">${label}</div>
<div style="flex:1;background:rgba(255,255,255,0.04);border-radius:3px;height:18px;position:relative;overflow:hidden">
<div style="position:absolute;top:0;left:0;height:100%;width:${pct}%;background:${barColor(pct)};opacity:0.4"></div>
<div style="position:absolute;top:0;left:0;height:100%;width:100%;display:flex;align-items:center;justify-content:space-between;padding:0 8px;font-size:11px">
<span class="mono" style="color:var(--text)">${pct}%</span>
<span class="dim" style="font-size:10px">${count}/${total}</span>
</div>
</div>
</div>
`).join('');
const sportTopHTML = (e.by_sport || []).slice(0, 8).map(s =>
`<div style="display:flex;justify-content:space-between;font-size:12px;padding:3px 0">
<span class="dim">${s.sport}</span><span class="mono">${s.broj}</span>
</div>`).join('');
const regionHTML = (e.by_region || []).map(r => {
const pctR = ((r.broj / e.klubova_total) * 100).toFixed(0);
return `<div style="display:flex;justify-content:space-between;font-size:12px;padding:3px 0">
<span>${r.region}</span><span class="mono dim">${r.broj} (${pctR}%)</span></div>`;
}).join('');
return `<div style="background:linear-gradient(135deg,rgba(59,130,196,0.08),rgba(245,158,11,0.05));border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:16px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
<h3 style="margin:0;font-size:14px;color:var(--text)">🌐 Sport Ekosustav PGŽ — FINA Registar Coverage</h3>
<span class="bdg gold" style="font-size:11px">${e.klubova_total} sport klubova</span>
</div>
<div class="ekosustav-grid" style="display:grid;grid-template-columns:1fr 280px;gap:20px">
<div>${coverageHTML}</div>
<div>
<div style="margin-bottom:10px">
<div style="font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px">Po regiji</div>
${regionHTML}
</div>
<div>
<div style="font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.5px;margin:10px 0 6px">Top sportovi</div>
${sportTopHTML}
</div>
</div>
</div>
</div>`;
} catch(e) { return ''; }
}
async function pageDashboard() {
// ERP KPI cards above existing content
const c = document.getElementById('content');
const erpHeader = document.createElement('div');
erpHeader.id = 'erp-kpi-grid';
erpHeader.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px;margin-bottom:16px';
erpHeader.innerHTML = '<div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px;text-align:center"><div style="font-size:11px;color:#475569;text-transform:uppercase;margin-bottom:4px">Klubovi</div><div id="kpi-klubovi" style="font-size:24px;font-weight:700;color:#3b82f6">—</div></div><div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px;text-align:center"><div style="font-size:11px;color:#475569;text-transform:uppercase;margin-bottom:4px">Savezi</div><div id="kpi-savezi" style="font-size:24px;font-weight:700;color:#06b6d4">—</div></div><div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px;text-align:center"><div style="font-size:11px;color:#475569;text-transform:uppercase;margin-bottom:4px">Sportaši</div><div id="kpi-sportasi" style="font-size:24px;font-weight:700;color:#22c55e">—</div></div><div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px;text-align:center"><div style="font-size:11px;color:#475569;text-transform:uppercase;margin-bottom:4px">Nositelji ★</div><div id="kpi-nositelji" style="font-size:24px;font-weight:700;color:#f59e0b">—</div></div><div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px;text-align:center"><div style="font-size:11px;color:#475569;text-transform:uppercase;margin-bottom:4px">Proračun</div><div id="kpi-proracun" style="font-size:24px;font-weight:700;color:#a78bfa">—</div></div><div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px;text-align:center"><div style="font-size:11px;color:#475569;text-transform:uppercase;margin-bottom:4px">Dokumenti</div><div id="kpi-dokumenti" style="font-size:24px;font-weight:700;color:#f97316">—</div></div>';
if (c && !document.getElementById('erp-kpi-grid')) c.prepend(erpHeader);
// Load KPIs
api('/api/dashboard').then(d => {
if (!d) return;
document.getElementById('kpi-klubovi') && (document.getElementById('kpi-klubovi').textContent = d.aktivnih_klubova || '—');
document.getElementById('kpi-savezi') && (document.getElementById('kpi-savezi').textContent = d.aktivnih_saveza || '—');
document.getElementById('kpi-sportasi') && (document.getElementById('kpi-sportasi').textContent = (d.aktivnih_clanova||0).toLocaleString('hr-HR'));
document.getElementById('kpi-nositelji') && (document.getElementById('kpi-nositelji').textContent = d.nositelja_kvalitete || '—');
if (d.proracun_aktualni) document.getElementById('kpi-proracun') && (document.getElementById('kpi-proracun').textContent = '€' + (d.proracun_aktualni/1e6).toFixed(2) + 'M');
});
api('/api/v2/dokumenti/list').then(d => {
const n = (d && (d.rows||d||[]).length) || 0;
document.getElementById('kpi-dokumenti') && (document.getElementById('kpi-dokumenti').textContent = n);
}).catch(()=>{});
setTopbar('PGŽ Sportski savez', 'Operativni pregled');
const c = document.getElementById('content');
c.innerHTML = '<div class="loader">Učitavanje…</div>';
const ekoHTML = await fetchEkosustav();
try {
const godina = state.filters.godina || 2026;
const savez = state.filters.savez_id || '';
const region = state.filters.region || '';
const [d, savezi] = await Promise.all([
api(`/api/dashboard?godina=${godina}` + (savez?`&savez_id=${savez}`:'') + (region?`&region=${region}`:'')),
api('/api/savezi')
]);
// Update alert badges
const totalA = (d.critical_alerts||0) + (d.warning_alerts||0);
['b-alertovi','b-alertovi-m'].forEach(id => {
const b = document.getElementById(id);
if (b) {
if (totalA>0) { b.textContent = totalA; b.style.display='inline-block'; b.className = d.critical_alerts>0?'b':'b warn'; }
else b.style.display='none';
}
});
const proracun = d.proracun_trend || [];
const procY = proracun.map(p => p.godina);
const procV = proracun.map(p => parseFloat(p.ukupno||0));
const ts = d.trend_savezi || [];
const trBy = {};
ts.forEach(r => { (trBy[r.naziv]=trBy[r.naziv]||[]).push({godina:r.godina, val:r.registriranih}); });
const allG = [...new Set(ts.map(r=>r.godina))].sort();
const trSer = Object.entries(trBy).slice(0,5).map(([n,da]) => ({label:n, data: allG.map(g=>(da.find(x=>x.godina===g)||{}).val||0)}));
const topSv = (d.top_savezi||[]).slice(0,8);
const noslist = d.nositelji||[];
const ls = d.lijec_status||{};
const lsV = [parseInt(ls.validni||0), parseInt(ls.uskoro_isteka||0), parseInt(ls.istekli||0), parseInt(ls.bez_termina||0)];
const zzjz = d.zzjz||{};
const kat = d.kategorije||[];
const katC = ['#4A9EFF','#D4A852','#A78BFA','#F472B6','#2DD4BF','#22D3EE'];
const totK = kat.reduce((s,k)=>s+parseInt(k.cnt||0),0);
const cG = d.clanarine_godine||[];
c.innerHTML = `
${ekoHTML || ''}
<div class="fbar">
<div class="fbar-t">⚙ Parametri</div>
<div class="fgrid">
<div class="fitem"><label>Godina</label>
<select onchange="state.filters.godina=this.value;render()">
${[2026,2025,2024,2023,2022,2021,2020].map(y=>`<option value="${y}" ${y==godina?'selected':''}>${y}.</option>`).join('')}
</select>
</div>
<div class="fitem"><label>Savez</label>
<select onchange="state.filters.savez_id=this.value;render()">
<option value="">— Svi savezi —</option>
${(savezi.rows||[]).map(s=>`<option value="${s.id}" ${s.id==savez?'selected':''}>${s.naziv}</option>`).join('')}
</select>
</div>
<div class="fitem"><label>Regija</label>
<select onchange="state.filters.region=this.value;render()">
<option value="">— Sve —</option>
${['Rijeka','Zaleđe','Primorje','Gorski kotar','Otoci'].map(r=>`<option value="${r}" ${r==region?'selected':''}>${r}</option>`).join('')}
</select>
</div>
<div class="fitem"><label>&nbsp;</label>
<button class="btn sec" onclick="state.filters={};render()">↻ Reset</button>
</div>
</div>
</div>
${d.critical_alerts>0?`<div class="ban crit"><b>⚠</b> ${d.critical_alerts} kritičnih alerta</div>`:''}
${d.warning_alerts>0?`<div class="ban warn"><b>⚠</b> ${d.warning_alerts} upozorenja</div>`:''}
<div class="sect">Ključni indikatori</div>
<div class="grid g4">
<div class="card acc"><div class="stat-l">Aktivni savezi</div><div class="stat-v">${d.aktivnih_saveza}</div><div class="stat-d">županijska razina</div></div>
<div class="card acc"><div class="stat-l">Aktivni klubovi</div><div class="stat-v">${d.aktivnih_klubova}</div><div class="stat-d">u 28 saveza</div></div>
<div class="card gold"><div class="stat-l">Nositelji kvalitete</div><div class="stat-v">${d.nositelja_kvalitete}</div><div class="stat-d">elitni klubovi</div></div>
<div class="card ok"><div class="stat-l">Proračun ${godina}</div><div class="stat-v sm">${fmtEur(d.proracun_aktualni)}</div><div class="stat-d up">↗ +38% YoY</div></div>
</div>
<div class="grid g4" style="margin-top:12px">
<div class="card"><div class="stat-l">Registr. sportaši</div><div class="stat-v">${fmt(d.registriranih_sportasa)}</div></div>
<div class="card"><div class="stat-l">Treneri</div><div class="stat-v">${fmt(d.trenera)}</div></div>
<div class="card"><div class="stat-l">Reprezentativci</div><div class="stat-v">${fmt(d.reprezentativaca)}</div></div>
<div class="card"><div class="stat-l">Aktivni članovi</div><div class="stat-v">${fmt(d.aktivnih_clanova)}</div></div>
</div>
<div class="sect">Financije ${godina}.</div>
<div class="grid g3">
<div class="card ok"><div class="stat-l">Naplaćeno</div><div class="stat-v sm">${fmtEur(d.naplaceno_clanarine_god)}</div></div>
<div class="card warn"><div class="stat-l">Dug članarine</div><div class="stat-v sm">${fmtEur(d.dug_clanarine_god)}</div></div>
<div class="card acc"><div class="stat-l">ZZJZ isplata</div><div class="stat-v sm">${fmtEur(d.zzjz_isplata_god)}</div></div>
</div>
<div class="sect">Liječnički pregledi</div>
<div class="grid g3">
<div class="card crit"><div class="stat-l">Istekli</div><div class="stat-v">${fmt(d.isteki_lijecnicki)}</div></div>
<div class="card warn"><div class="stat-l">Ističu uskoro</div><div class="stat-v">${fmt(d.lijecnicki_uskoro_istek)}</div></div>
<div class="card crit"><div class="stat-l">Critical alertovi</div><div class="stat-v">${fmt(d.critical_alerts)}</div></div>
</div>
<div class="sect">Vizualne analize</div>
<div class="grid g2">
<div class="card">
<div class="ct">📈 Proračun PGŽ za sport <span class="meta">2016—2026</span></div>
${lineChart([{label:'Ukupno', data:procV}], procY, 600, 220)}
</div>
<div class="card">
<div class="ct">📊 Top 5 saveza · trend reg. <span class="meta">${allG[0]||''}—${allG[allG.length-1]||''}</span></div>
${trSer.length?lineChart(trSer, allG, 600, 220):'<div class="empty">Bez podataka</div>'}
</div>
</div>
<div class="grid g2" style="margin-top:12px">
<div class="card">
<div class="ct">🏆 Top saveza · reg. ${godina>2024?2024:godina}.</div>
${barChart(topSv, s=>s.naziv, s=>s.registriranih||0)}
</div>
<div class="card">
<div class="ct">⭐ Nositelji kvalitete ${godina>2025?2025:godina}.</div>
${noslist.length?barChart(noslist, n=>n.naziv_kluba, n=>parseFloat(n.iznos||0), 'gold', fmtEur):'<div class="empty">Bez podataka</div>'}
</div>
</div>
<div class="grid g3" style="margin-top:12px">
<div class="card">
<div class="ct">🏥 Status pregleda</div>
<div class="donut-w">
${donut(lsV, ['Validni','Uskoro','Istekli','Bez termina'], ['#2DD4BF','#F59E0B','#EF4444','#6B7A99'], lsV.reduce((a,b)=>a+b,0), 'pregleda')}
<div class="lg">
<div class="it"><div class="sw" style="background:#2DD4BF"></div><span class="lname">Validni</span><span class="lval">${lsV[0]}</span></div>
<div class="it"><div class="sw" style="background:#F59E0B"></div><span class="lname">Uskoro</span><span class="lval">${lsV[1]}</span></div>
<div class="it"><div class="sw" style="background:#EF4444"></div><span class="lname">Istekli</span><span class="lval">${lsV[2]}</span></div>
<div class="it"><div class="sw" style="background:#6B7A99"></div><span class="lname">Bez termina</span><span class="lval">${lsV[3]}</span></div>
</div>
</div>
</div>
<div class="card">
<div class="ct">⚕ ZZJZ podjela</div>
<div class="donut-w">
${donut([parseFloat(zzjz.zzjz_udio||0), parseFloat(zzjz.klub_udio||0), parseFloat(zzjz.clan_udio||0)],
['ZZJZ','Klub','Član'], ['#2DD4BF','#4A9EFF','#D4A852'],
fmt(parseFloat(zzjz.total||0)), 'EUR')}
<div class="lg">
<div class="it"><div class="sw" style="background:#2DD4BF"></div><span class="lname">ZZJZ</span><span class="lval">${fmtEur(zzjz.zzjz_udio)}</span></div>
<div class="it"><div class="sw" style="background:#4A9EFF"></div><span class="lname">Klub</span><span class="lval">${fmtEur(zzjz.klub_udio)}</span></div>
<div class="it"><div class="sw" style="background:#D4A852"></div><span class="lname">Član</span><span class="lval">${fmtEur(zzjz.clan_udio)}</span></div>
</div>
</div>
</div>
<div class="card">
<div class="ct">👥 Kategorije</div>
<div class="donut-w">
${donut(kat.map(k=>parseInt(k.cnt)), kat.map(k=>k.kategorija), katC, totK, 'članova')}
<div class="lg">${kat.map((k,i)=>`<div class="it"><div class="sw" style="background:${katC[i%katC.length]}"></div><span class="lname">${k.kategorija||''}</span><span class="lval">${k.cnt}</span></div>`).join('')}</div>
</div>
</div>
</div>
${cG.length?`<div class="card" style="margin-top:12px">
<div class="ct">💰 Članarine kroz godine · propisano vs naplaćeno vs dug</div>
${lineChart([
{label:'Propisano', data:cG.map(c=>parseFloat(c.propisano||0))},
{label:'Naplaćeno', data:cG.map(c=>parseFloat(c.placeno||0))},
{label:'Dug', data:cG.map(c=>parseFloat(c.dug||0))}
], cG.map(c=>c.godina), 1200, 220)}</div>`:''}
`;
} catch (e) { c.innerHTML = `<div class="ban crit"><b>Greška:</b> ${e.message}</div>`; }
}
// ========== ANALYTICS FINAL — Modern Dropdown UI ==========
let _anaOpts = null;
async function pageAnalytics() {
const el = document.getElementById('content');
el.innerHTML = '<div class="loader">Ucitavanje...</div>';
const [opts, trend] = await Promise.all([
api('/api/v2/analytics/filter-options').catch(()=>{}),
api('/api/v2/analytics/proracun-trend').catch(()=>[])
]);
_anaOpts = opts || {};
const last = (trend||[]).slice(-1)[0] || {};
const rastPct = last.rast_pct || 0;
el.innerHTML = `
<div class="page-h">
<h2>Bodovanje &amp; Analitika</h2>
<p class="muted">Sustav bodovanja za dodjelu proracunskih sredstava PGZ · HOO kriteriji</p>
</div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:16px">
${[
['Proracun 2026', _eurF(last.ukupno_eur), rastPct > 0 ? '#22c55e' : '#ef4444'],
['Rast', (rastPct>0?'+':'')+rastPct+'%', rastPct>0?'#22c55e':'#ef4444'],
['Saveza rangirano', '<span id="kpi-n">—</span>', 'var(--text)'],
['Avg evid. %', '<span id="kpi-evid">—</span>', 'var(--text)'],
].map(([label,val,col])=>`
<div class="card" style="padding:12px 14px;text-align:center;background:var(--bg4)">
<div style="font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:5px">${label}</div>
<div style="font-size:18px;font-weight:700;font-family:monospace;color:${col}">${val}</div>
</div>`).join('')}
</div>
<div class="card" style="padding:10px 12px;margin-bottom:12px;display:flex;flex-wrap:wrap;gap:8px;align-items:center;background:var(--bg3)">
<div style="display:flex;align-items:center;gap:6px">
<span style="font-size:11px;color:var(--muted);white-space:nowrap">Prikaz</span>
<select id="av" class="i" onchange="_anaLoad()" style="min-width:150px">
<option value="savezi">Bodovanje saveza</option>
<option value="klubovi">Bodovanje klubova</option>
<option value="trend">Trend proracuna</option>
</select>
</div>
<div style="display:flex;align-items:center;gap:6px">
<span style="font-size:11px;color:var(--muted)">Sport</span>
<select id="aspt" class="i" onchange="_anaLoad()" style="min-width:130px">
<option value="">Svi sportovi</option>
${(_anaOpts.sportovi_savezi||[]).map(s=>`<option>${s}</option>`).join('')}
</select>
</div>
<div style="display:flex;align-items:center;gap:6px">
<span style="font-size:11px;color:var(--muted)">Grad</span>
<select id="agrd" class="i" onchange="_anaLoad()" style="min-width:110px">
<option value="">Svi</option>
${(_anaOpts.gradovi||[]).map(g=>`<option>${g}</option>`).join('')}
</select>
</div>
<div style="display:flex;align-items:center;gap:6px">
<span style="font-size:11px;color:var(--muted)">Min. cl.</span>
<select id="amc" class="i" onchange="_anaLoad()">
<option value="0">Svi</option>
<option value="10">10+</option>
<option value="50">50+</option>
<option value="100">100+</option>
<option value="500">500+</option>
</select>
</div>
<button class="btn" onclick="_anaCsv()" style="margin-left:auto;font-size:11px;padding:5px 12px">↓ CSV</button>
<button onclick="navigate('dataQuality')" style="background:#0f172a;border:1px solid #f59e0b44;color:#f59e0b;padding:5px 10px;border-radius:6px;cursor:pointer;font-size:11px">📊 Kompletnost</button>
<button onclick="navigate('dms')" style="background:#0f172a;border:1px solid #3b82f644;color:#3b82f6;padding:5px 10px;border-radius:6px;cursor:pointer;font-size:11px">📁 DMS</button>
</div>
<div id="anaContent" class="loader">Ucitavanje...</div>
<div id="anaDrill" style="display:none;margin-top:12px">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
<button class="btn" onclick="_drillClose()" style="font-size:11px;padding:4px 10px">&larr;</button>
<span id="drillTitle" style="font-size:14px;font-weight:700;color:var(--text)"></span>
</div>
<div id="drillContent" class="loader">Ucitavanje...</div>
</div>`;
_anaLoad();
}
function _eurF(n) {
if (!n && n!==0) return '—';
return new Intl.NumberFormat('hr-HR',{style:'currency',currency:'EUR',maximumFractionDigits:0}).format(n);
}
function _scoreBar(s) {
s = Math.max(0, Math.min(100, s||0));
const c = s>=60?'#22c55e':s>=30?'#f59e0b':'#ef4444';
return `<div style="display:flex;align-items:center;gap:5px">
<div style="width:50px;height:5px;background:rgba(255,255,255,0.08);border-radius:3px;overflow:hidden;flex-shrink:0">
<div style="width:${s}%;height:100%;background:${c}"></div>
</div>
<b style="font-size:12px;font-family:monospace;color:${c}">${s}</b>
</div>`;
}
async function _anaLoad() {
const view = document.getElementById('av')?.value||'savezi';
const sport = document.getElementById('aspt')?.value||'';
const grad = document.getElementById('agrd')?.value||'';
const mc = document.getElementById('amc')?.value||'0';
const el = document.getElementById('anaContent'); if(!el)return;
el.style.display='block';
document.getElementById('anaDrill').style.display='none';
el.innerHTML='<div class="loader">Ucitavanje...</div>';
if (view==='trend') {
const d=await api('/api/v2/analytics/proracun-trend').catch(()=>[]);
_renderTrend(d,el); return;
}
const ep = view==='savezi'
? '/api/v2/analytics/budget-score?'+new URLSearchParams({sport,min_clanova:mc})
: '/api/v2/analytics/klub-score?'+new URLSearchParams({sport,min_clanova:mc});
const data = await api(ep).catch(()=>[]);
const rows = grad ? (data||[]).filter(r=>r.grad===grad) : (data||[]);
if(view==='savezi') {
document.getElementById('kpi-n').textContent=rows.length;
if(rows.length) {
const avg=rows.reduce((a,r)=>a+(r.pct_s_dob||0),0)/rows.length;
const kpi=document.getElementById('kpi-evid');
kpi.textContent=avg.toFixed(1)+'%';
kpi.style.color=avg>50?'#22c55e':avg>20?'#f59e0b':'#ef4444';
}
_renderSavezi(rows,el);
} else {
_renderKlubovi(rows,el);
}
}
function _renderSavezi(data,el) {
if(!data?.length){el.innerHTML='<p class="muted" style="padding:20px">Nema saveza za filtere.</p>';return;}
el.innerHTML=`<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:12px">
<thead><tr style="background:var(--bg3)">
${['#','Savez','Reg.','Sustav','Klubovi','Treneri','Repr.','Evid.%','Score'].map(h=>`<th style="padding:7px 10px;color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.5px;text-align:${['Savez'].includes(h)?'left':'right'};${h==='Score'||h==='#'?'text-align:left':''}">${h}</th>`).join('')}
</tr></thead>
<tbody>${data.map((r,i)=>`
<tr style="border-top:1px solid var(--border);cursor:pointer;transition:background .1s" onclick="drillSavez(${r.id},'${(r.naziv||'').replace(/'/g,"\\'")}')" onmouseover="this.style.background='rgba(255,255,255,0.025)'" onmouseout="this.style.background=''">
<td style="padding:7px 10px;color:var(--muted);font-size:11px;font-family:monospace">${i+1}</td>
<td style="padding:7px 10px">
<div style="font-weight:500;color:var(--text)">${r.naziv||'—'}</div>
${r.sport?`<div style="font-size:10px;color:var(--muted)">${r.sport}${r.grad?' · '+r.grad:''}</div>`:''}
</td>
<td style="padding:7px 10px;text-align:right;font-family:monospace">${r.registriranih||'—'}</td>
<td style="padding:7px 10px;text-align:right;font-family:monospace">${r.clanova_u_sustavu||0}</td>
<td style="padding:7px 10px;text-align:right;font-family:monospace">${r.klubova||0}</td>
<td style="padding:7px 10px;text-align:right;font-family:monospace">${r.trenera||0}</td>
<td style="padding:7px 10px;text-align:right;font-family:monospace">${r.reprezentativaca||0}</td>
<td style="padding:7px 10px;text-align:right;font-size:11px;color:${(r.pct_s_dob||0)>=50?'#22c55e':(r.pct_s_dob||0)>=20?'#f59e0b':'#ef4444'}">${r.pct_s_dob||0}%</td>
<td style="padding:7px 10px">${_scoreBar(r.score_ukupno)}</td>
</tr>`).join('')}
</tbody></table></div>
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:10px;padding:8px 10px;background:var(--bg3);border-radius:6px;font-size:11px;color:var(--muted)">
<span>Clanovi <b>25</b></span><span>Klubovi <b>15</b></span><span>Treneri <b>15</b></span><span>Evidencija <b>20</b></span><span>Repr. <b>15</b></span>
<span style="margin-left:auto;color:var(--accent)">Klik = detalji</span>
</div>`;
}
function _renderKlubovi(data,el) {
if(!data?.length){el.innerHTML='<p class="muted" style="padding:20px">Nema klubova.</p>';return;}
el.innerHTML=`<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:12px">
<thead><tr style="background:var(--bg3)">
${['#','Klub','Savez','Clan.','M','Z','DOB%','Score'].map(h=>`<th style="padding:7px 10px;color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.5px;text-align:${['Klub','Savez'].includes(h)?'left':'right'};${h==='#'||h==='Score'?'text-align:left':''}">${h}</th>`).join('')}
</tr></thead>
<tbody>${data.map((r,i)=>`<tr style="border-top:1px solid var(--border)">
<td style="padding:6px 10px;color:var(--muted);font-size:11px;font-family:monospace">${i+1}</td>
<td style="padding:6px 10px"><div style="color:var(--text)">${r.naziv||'—'}</div><div style="font-size:10px;color:var(--muted)">${r.grad||''}</div></td>
<td style="padding:6px 10px;font-size:11px;color:var(--text2)">${r.savez||'—'}</td>
<td style="padding:6px 10px;text-align:right;font-family:monospace">${r.n_clanova||0}</td>
<td style="padding:6px 10px;text-align:right;font-family:monospace;color:var(--text2)">${r.muski||0}</td>
<td style="padding:6px 10px;text-align:right;font-family:monospace;color:var(--text2)">${r.zenski||0}</td>
<td style="padding:6px 10px;text-align:right;font-size:11px;color:${(r.pct_dob||0)>=50?'#22c55e':(r.pct_dob||0)>=20?'#f59e0b':'#ef4444'}">${r.pct_dob||0}%</td>
<td style="padding:6px 10px">${_scoreBar(r.score)}</td>
</tr>`).join('')}</tbody>
</table></div>`;
}
function _renderTrend(data,el) {
if(!data?.length){el.innerHTML='<p class="muted">Nema podataka.</p>';return;}
const maxV=Math.max(...data.map(r=>r.ukupno_eur||0));
el.innerHTML=`<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:12px">
<thead><tr style="background:var(--bg3)">
<th style="text-align:left;padding:7px 10px;color:var(--muted);font-size:10px;text-transform:uppercase">Godina</th>
<th style="text-align:right;padding:7px 10px;color:var(--muted);font-size:10px;text-transform:uppercase">PGZ</th>
<th style="text-align:right;padding:7px 10px;color:var(--muted);font-size:10px;text-transform:uppercase">Ministarstvo</th>
<th style="text-align:right;padding:7px 10px;color:var(--muted);font-size:10px;text-transform:uppercase">Ukupno</th>
<th style="text-align:right;padding:7px 10px;color:var(--muted);font-size:10px;text-transform:uppercase">Rast</th>
<th style="padding:7px 10px;color:var(--muted);font-size:10px;text-transform:uppercase;min-width:120px">Vizual</th>
</tr></thead>
<tbody>${[...data].reverse().map(r=>{
const pct=maxV>0?(r.ukupno_eur||0)/maxV*100:0;
const rc=(r.rast_pct||0)>0?'#22c55e':'#ef4444';
return `<tr style="border-top:1px solid var(--border)">
<td style="padding:7px 10px;font-weight:700;color:var(--text)">${r.godina}</td>
<td style="padding:7px 10px;text-align:right;font-family:monospace">${_eurF(r.pgz_ukupno_eur)}</td>
<td style="padding:7px 10px;text-align:right;font-family:monospace;color:var(--text2)">${_eurF(r.ministarstvo_eur)}</td>
<td style="padding:7px 10px;text-align:right;font-family:monospace;font-weight:700">${_eurF(r.ukupno_eur)}</td>
<td style="padding:7px 10px;text-align:right;font-family:monospace;color:${rc}">${r.rast_pct!=null?(r.rast_pct>0?'+':'')+r.rast_pct+'%':'—'}</td>
<td style="padding:7px 10px"><div style="height:8px;background:rgba(255,255,255,0.06);border-radius:2px;overflow:hidden">
<div style="width:${pct}%;height:100%;background:var(--accent);opacity:.75"></div>
</div></td>
</tr>`;
}).join('')}</tbody>
</table></div>`;
}
async function drillSavez(id, naziv) {
document.getElementById('anaContent').style.display='none';
const drill=document.getElementById('anaDrill');
drill.style.display='block';
document.getElementById('drillTitle').textContent=naziv;
const el=document.getElementById('drillContent');
el.innerHTML='<div class="loader">Ucitavanje...</div>';
const d=await api('/api/v2/analytics/savez-drill?savez_id='+id).catch(()=>null);
if(!d||d.error){el.innerHTML='<p class="muted">Nema podataka.</p>';return;}
const s=d.savez||{}, cs=d.clanovi_spol||{};
const pctDob=cs.ukupno>0?((cs.s_dob||0)/cs.ukupno*100).toFixed(1)+'%':'—';
el.innerHTML=`
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin-bottom:12px">
${[['Clanovi',cs.ukupno||0],['Muski',cs.muski||0],['Zenski',cs.zenski||0],['DOB %',pctDob],['Sport',s.sport||'—'],['Sjediste',s.grad||'—']].map(([k,v])=>`
<div class="card" style="padding:10px 12px;background:var(--bg4);text-align:center">
<div style="font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:4px">${k}</div>
<div style="font-size:15px;font-weight:700;color:var(--text)">${v}</div>
</div>`).join('')}
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px">
<div>
<div style="font-size:10px;font-weight:700;color:var(--accent);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px">Statistika po godinama</div>
<table style="width:100%;border-collapse:collapse;font-size:11px">
<thead><tr style="background:var(--bg3)"><th style="padding:4px 8px;text-align:left">God.</th><th style="padding:4px 8px;text-align:right">Reg.</th><th style="padding:4px 8px;text-align:right">Tren.</th><th style="padding:4px 8px;text-align:right">Repr.</th></tr></thead>
<tbody>${(d.statistike||[]).map(r=>`<tr style="border-top:1px solid var(--border)"><td style="padding:4px 8px;font-weight:600">${r.godina}</td><td style="padding:4px 8px;text-align:right;font-family:monospace">${r.registriranih||0}</td><td style="padding:4px 8px;text-align:right;font-family:monospace">${r.trenera||0}</td><td style="padding:4px 8px;text-align:right;font-family:monospace">${r.reprezentativaca||0}</td></tr>`).join('')||'<tr><td colspan="4" style="padding:8px;color:var(--muted)">—</td></tr>'}
</tbody>
</table>
</div>
<div>
<div style="font-size:10px;font-weight:700;color:var(--accent);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px">Top klubovi</div>
<table style="width:100%;border-collapse:collapse;font-size:11px">
<thead><tr style="background:var(--bg3)"><th style="padding:4px 8px;text-align:left">Klub</th><th style="padding:4px 8px;text-align:right">Cl.</th><th style="padding:4px 8px;text-align:right">DOB</th></tr></thead>
<tbody>${(d.klubovi||[]).slice(0,8).map(k=>{
const p=k.n_clanova>0?((k.s_dob||0)/k.n_clanova*100).toFixed(0):'?';
return`<tr style="border-top:1px solid var(--border)"><td style="padding:4px 8px;color:var(--text2)">${k.naziv||'—'}</td><td style="padding:4px 8px;text-align:right;font-family:monospace">${k.n_clanova||0}</td><td style="padding:4px 8px;text-align:right;font-family:monospace;color:${p>=50?'#22c55e':p>=20?'#f59e0b':'#ef4444'}">${p}%</td></tr>`;
}).join('')||'<tr><td colspan="3" style="padding:8px;color:var(--muted)">—</td></tr>'}
</tbody>
</table>
</div>
</div>
${d.javne_potrebe?.length?`
<div style="font-size:10px;font-weight:700;color:var(--accent);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px">Javne dodjele</div>
<table style="width:100%;border-collapse:collapse;font-size:11px">
<thead><tr style="background:var(--bg3)"><th style="padding:4px 8px;text-align:left">Godina</th><th style="padding:4px 8px;text-align:left">Namjena</th><th style="padding:4px 8px;text-align:right">EUR</th></tr></thead>
<tbody>${d.javne_potrebe.map(j=>`<tr style="border-top:1px solid var(--border)"><td style="padding:4px 8px;font-weight:600">${j.godina}</td><td style="padding:4px 8px;color:var(--text2)">${j.naslov||j.vrsta||'—'}</td><td style="padding:4px 8px;text-align:right;font-family:monospace;font-weight:600">${_eurF(j.iznos_eur)}</td></tr>`).join('')}
</tbody>
</table>`:
'<div style="font-size:11px;color:var(--muted)">Nema evidencije javnih dodjela.</div>'}`;
}
function _drillClose() {
document.getElementById('anaDrill').style.display='none';
document.getElementById('anaContent').style.display='block';
}
function _anaCsv() {
const t=document.querySelector('#anaContent table'); if(!t)return;
const rows=[...t.querySelectorAll('tr')].map(r=>[...r.querySelectorAll('th,td')].map(c=>'"'+c.textContent.trim().replace(/"/g,'""')+'"').join(','));
const a=document.createElement('a');
a.href='data:text/csv;charset=utf-8,'+encodeURIComponent('\uFEFF'+rows.join('\n'));
a.download='pgz_bodovanje_'+new Date().toISOString().slice(0,10)+'.csv';
a.click();
}
async function pageGraf() {
setTopbar && setTopbar('3D Graf', 'PGŽ Sport Intelligence · Savezi · Klubovi · Sportaši');
const el = document.getElementById('content');
el.innerHTML = `
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:10px" id="g3d-ctrl">
<select id="g3d-mode" onchange="graf3dLoad()" style="background:#0d1117;color:#e2e8f0;border:1px solid #1e293b;padding:6px 10px;border-radius:6px;font-size:12px">
<option value="sport">Savezi → Klubovi (Sport)</option>
<option value="osobe">Savezi → Osobe</option>
<option value="geo">Gradovi → Klubovi</option>
</select>
<select id="g3d-sport" onchange="graf3dLoad()" style="background:#0d1117;color:#e2e8f0;border:1px solid #1e293b;padding:6px 10px;border-radius:6px;font-size:12px">
<option value="">Svi sportovi</option>
${['nogomet','košarka','odbojka','rukomet','bočanje','skijanje','vaterpolo','atletika','tenis','jedrenje','karate','plivanje'].map(s=>`<option value="${s}">${s}</option>`).join('')}
</select>
<span id="g3d-info" style="color:#475569;font-size:11px;margin-left:auto"></span>
</div>
<div id="g3d-container" style="width:100%;height:calc(100vh-200px);min-height:500px;border-radius:12px;overflow:hidden;border:1px solid #1e293b;background:#0d0d0d;position:relative">
<div id="g3d-loader" style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#475569;font-size:14px;background:#0d0d0d;z-index:5">
Učitavam 3D graf...
</div>
</div>
<div id="g3d-legend" style="display:flex;gap:16px;margin-top:8px;font-size:11px;color:#64748b;padding:0 4px">
<span><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#06b6d4;margin-right:4px"></span>Savez/Grad</span>
<span><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#22c55e;margin-right:4px"></span>Klub</span>
<span><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#ec4899;margin-right:4px"></span>Osoba</span>
<span><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#f59e0b;margin-right:4px"></span>Nositelj ★</span>
</div>`;
await graf3dLoadLib();
await graf3dLoad();
}
let _g3d = null;
let _g3dLib = false;
async function graf3dLoadLib() {
if (_g3dLib) return;
await Promise.all([
new Promise(r => {
const s = document.createElement('script');
s.src = 'https://unpkg.com/3d-force-graph@1.73.2/dist/3d-force-graph.min.js';
s.onload = r; document.head.appendChild(s);
}),
new Promise(r => {
const s = document.createElement('script');
s.src = 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js';
s.onload = r; document.head.appendChild(s);
})
]);
_g3dLib = true;
}
async function graf3dLoad() {
const mode = document.getElementById('g3d-mode')?.value || 'sport';
const sport = document.getElementById('g3d-sport')?.value || '';
const info = document.getElementById('g3d-info');
const loader = document.getElementById('g3d-loader');
if (loader) loader.style.display = 'flex';
const [savezi, klubovi, clanovi] = await Promise.all([
api('/api/savezi').catch(()=>({rows:[]})),
api('/api/klubovi?' + (sport?'sport='+encodeURIComponent(sport):'')).catch(()=>({rows:[]})),
mode === 'osobe' ? api('/api/clanovi?limit=200').catch(()=>({rows:[]})) : Promise.resolve({rows:[]})
]);
const saveziArr = savezi.rows || savezi || [];
const klubArr = (klubovi.rows || klubovi || []).filter(k=>k.naziv && k.naziv.length > 2);
const clanoviArr = clanovi.rows || clanovi || [];
const nodes = [], links = [];
const nodeMap = {};
if (mode === 'sport') {
// Savezi → Klubovi force graph
saveziArr.slice(0, 40).forEach(s => {
const id = 'savez_' + s.id;
nodes.push({ id, name: (s.naziv||'Savez').slice(0,30), group:'savez', val:20, color:'#06b6d4' });
nodeMap[s.id] = id;
});
klubArr.slice(0, 300).forEach(k => {
const id = 'klub_' + k.id;
const col = k.nositelj_kvalitete ? '#f59e0b' : '#22c55e';
nodes.push({ id, name: (k.naziv||'Klub').slice(0,25), group:'klub', val:k.nositelj_kvalitete?10:5, color:col, _data:k });
const savezId = nodeMap[k.savez_id];
if (savezId) links.push({ source: savezId, target: id, color: 'rgba(34,197,94,0.15)' });
});
} else if (mode === 'osobe') {
// Savezi → Osobe network
saveziArr.slice(0, 20).forEach(s => {
const id = 'savez_' + s.id;
nodes.push({ id, name: (s.naziv||'Savez').slice(0,30), group:'savez', val:25, color:'#06b6d4' });
nodeMap[s.id] = id;
});
clanoviArr.forEach(c => {
if (!c.ime && !c.prezime) return;
const name = ((c.prezime||'') + ' ' + (c.ime||'')).trim();
const id = 'clan_' + c.id;
nodes.push({ id, name: name.slice(0,20), group:'person', val:c.kategoriziran?12:4, color:c.reprezentativac?'#f59e0b':'#ec4899', _data:c });
const savezId = nodeMap[c.savez_id] || (nodes.length > 1 ? 'savez_' + saveziArr[0]?.id : null);
if (savezId) links.push({ source: savezId, target: id, color: 'rgba(236,72,153,0.1)' });
});
} else {
// Geo: Gradovi → Klubovi
const gradMap = {};
klubArr.forEach(k => { const g = k.grad||'Ostalo'; (gradMap[g]||(gradMap[g]=[])).push(k); });
Object.keys(gradMap).slice(0, 30).forEach(g => {
const id = 'grad_' + g;
nodes.push({ id, name: g, group:'savez', val:18, color:'#a78bfa' });
gradMap[g].slice(0, 20).forEach(k => {
const kid = 'klub_' + k.id;
nodes.push({ id:kid, name:(k.naziv||'').slice(0,22), group:'klub', val:k.nositelj_kvalitete?10:4, color:k.nositelj_kvalitete?'#f59e0b':'#22c55e', _data:k });
links.push({ source: id, target: kid, color:'rgba(167,139,250,0.15)' });
});
});
}
if (info) info.textContent = `${nodes.length} čvorova · ${links.length} veza`;
// Cleanup previous
if (_g3d) { try { _g3d._destructor(); } catch {} }
const container = document.getElementById('g3d-container');
if (!container) return;
container.innerHTML = '';
// Init 3D force graph
const ForceGraph3D = window.ForceGraph3D || window['3d-force-graph']?.default || window['ForceGraph3D'];
if (!ForceGraph3D) {
container.innerHTML = '<div style="color:#ef4444;padding:20px">3D knjižnica nije učitana. Refreshaj stranicu.</div>';
return;
}
const graph = ForceGraph3D()(container)
.width(container.clientWidth || 800)
.height(container.clientHeight || 550)
.graphData({ nodes, links })
.backgroundColor('#0d0d0d')
.nodeLabel(n => `<div style="background:rgba(5,8,16,0.92);border:1px solid rgba(0,212,255,0.25);border-radius:6px;padding:5px 10px;font-size:11px;color:#e2e8f0"><b style="color:${n.color}">${n.name}</b></div>`)
.nodeColor(n => n.color)
.nodeVal(n => (n.val || 5) * 3)
.nodeOpacity(0.9)
.nodeResolution(16)
.linkColor(l => l.color || 'rgba(255,255,255,0.05)')
.linkWidth(0.2)
.linkOpacity(0.5)
.onNodeClick(n => {
if (n._data?.id) {
if (n.group === 'klub') showKlub(n._data.id);
else if (n.group === 'person') navigate('sportas_' + n._data.id);
}
})
.onNodeHover(n => { container.style.cursor = n ? 'pointer' : 'grab'; });
graph.d3Force('charge')?.strength(-80);
graph.d3Force('link')?.distance(50);
_g3d = graph;
if (loader) loader.style.display = 'none';
}
async function initGraf3D() {
const sport = document.getElementById('gf-sport')?.value || '';
const mode = document.getElementById('gf-mode')?.value || 'savez';
const info = document.getElementById('gf-info');
if (info) info.textContent = 'Učitavam podatke...';
// Fetch data
const [savezi, klubovi] = await Promise.all([
api('/api/savezi').catch(()=>[]),
api('/api/klubovi' + (sport ? '?sport='+encodeURIComponent(sport) : '')).catch(()=>[])
]);
const klubArr = (klubovi.rows || klubovi || []);
const saveziArr = (savezi.rows || savezi || []);
if (info) info.textContent = `${saveziArr.length} saveza · ${klubArr.length} klubova`;
// Build graph nodes + edges
const nodes = [];
const edges = [];
const nodeMap = {};
if (mode === 'savez') {
// Center: PGŽ Sport
nodes.push({ id: 'pgz', label: 'PGŽ Sport', type: 'root', size: 3.0, color: 0xf59e0b, x:0, y:0, z:0 });
nodeMap['pgz'] = nodes.length - 1;
// Savezi kao planetarni ring
const sCount = Math.min(saveziArr.length, 30);
saveziArr.slice(0, sCount).forEach((s, i) => {
const angle = (i / sCount) * Math.PI * 2;
const r = 120 + Math.random() * 40;
const y = (Math.random() - 0.5) * 60;
const id = 's_' + s.id;
nodes.push({
id, label: (s.naziv||'Savez').replace('savez PGŽ','').replace('Savez PGŽ','').trim().slice(0,22),
type: 'savez', size: 1.4, color: 0x3b82f6,
x: Math.cos(angle)*r, y, z: Math.sin(angle)*r,
data: s
});
nodeMap[id] = nodes.length - 1;
edges.push({ from: 'pgz', to: id, color: 0x1e40af });
});
// Klubovi kao sateliti saveza
const colors3d = [0x22c55e, 0x10b981, 0x34d399, 0x6ee7b7];
klubArr.slice(0, 200).forEach((k, i) => {
const sid = 's_' + k.savez_id;
if (!nodeMap[sid] && !nodeMap['pgz']) return;
const parent = nodeMap[sid] !== undefined ? nodes[nodeMap[sid]] : nodes[0];
const angle = Math.random() * Math.PI * 2;
const r2 = 28 + Math.random() * 22;
const ky = parent.y + (Math.random()-0.5)*30;
const id = 'k_' + k.id;
nodes.push({
id, label: (k.naziv||'Klub').slice(0,18),
type: 'klub', size: 0.7 + (k.nositelj_kvalitete ? 0.4 : 0),
color: k.nositelj_kvalitete ? 0xf59e0b : colors3d[i%colors3d.length],
x: parent.x + Math.cos(angle)*r2, y: ky, z: parent.z + Math.sin(angle)*r2,
data: k
});
nodeMap[id] = nodes.length - 1;
edges.push({ from: sid !== 's_undefined' ? sid : 'pgz', to: id, color: 0x1d4d20 });
});
} else if (mode === 'sport') {
// Group by sport
const sportGroups = {};
klubArr.forEach(k => { const sp = k.sport||'ostalo'; (sportGroups[sp]||(sportGroups[sp]=[])).push(k); });
const sportColors = { 'nogomet':0xef4444,'košarka':0xf97316,'odbojka':0xa78bfa,
'rukomet':0x22c55e,'bočanje':0x06b6d4,'skijanje':0x93c5fd,'tenis':0xfbbf24,'ostalo':0x64748b };
const sports = Object.keys(sportGroups);
nodes.push({ id:'pgz',label:'PGŽ Sport',type:'root',size:3.0,color:0xf59e0b,x:0,y:0,z:0 });
sports.slice(0,20).forEach((sp,si) => {
const angle=(si/sports.length)*Math.PI*2;
const r=100;
const col = sportColors[sp] || 0x64748b;
const spid='sp_'+sp;
nodes.push({ id:spid, label:sp, type:'sport_cat', size:1.8, color:col,
x:Math.cos(angle)*r, y:(Math.random()-0.5)*40, z:Math.sin(angle)*r });
edges.push({ from:'pgz', to:spid, color:col });
const sx=Math.cos(angle)*r, sz=Math.sin(angle)*r;
sportGroups[sp].slice(0,15).forEach((k,ki) => {
const a2=Math.random()*Math.PI*2, r2=25+Math.random()*20;
nodes.push({ id:'k_'+k.id, label:(k.naziv||'').slice(0,16), type:'klub', size:0.65,
color:col, x:sx+Math.cos(a2)*r2, y:(Math.random()-0.5)*30, z:sz+Math.sin(a2)*r2, data:k });
edges.push({ from:spid, to:'k_'+k.id, color:col });
});
});
} else {
// Geografija
const geoMap = {};
klubArr.forEach(k => { const g=k.grad||'Ostalo'; (geoMap[g]||(geoMap[g]=[])).push(k); });
const geoPositions = {
'Rijeka':{x:0,z:0},'Opatija':{x:80,z:-30},'Krk':{x:120,z:60},
'Crikvenica':{x:-60,z:80},'Delnice':{x:-90,z:-60},'Čabar':{x:-130,z:-80},
'Mali Lošinj':{x:150,z:100},'Rab':{x:130,z:130},'Senj':{x:-100,z:120}
};
nodes.push({ id:'pgz',label:'PGŽ',type:'root',size:3.0,color:0xf59e0b,x:0,y:50,z:0 });
const geoKeys = Object.keys(geoMap);
geoKeys.slice(0,25).forEach((g,gi) => {
const pos = geoPositions[g] || { x:(Math.random()-0.5)*200, z:(Math.random()-0.5)*200 };
const gid = 'g_'+g;
nodes.push({ id:gid, label:g, type:'grad', size:1.5, color:0x06b6d4,
x:pos.x, y:0, z:pos.z });
edges.push({ from:'pgz', to:gid, color:0x0891b2 });
geoMap[g].slice(0,12).forEach((k,ki) => {
const a=Math.random()*Math.PI*2, r=20+Math.random()*18;
nodes.push({ id:'k_'+k.id, label:(k.naziv||'').slice(0,16), type:'klub', size:0.65,
color:0x22c55e, x:pos.x+Math.cos(a)*r, y:-10+(Math.random()-0.5)*20, z:pos.z+Math.sin(a)*r, data:k });
edges.push({ from:gid, to:'k_'+k.id, color:0x166534 });
});
});
}
render3DGraph(nodes, edges, mode);
}
function render3DGraph(nodes, edges, mode) {
if (!window.THREE) { console.error('Three.js not loaded'); return; }
const THREE = window.THREE;
const canvas = document.getElementById('graf3d-canvas');
const container = document.getElementById('graf3d-container');
const tooltip = document.getElementById('graf3d-tooltip');
const legend = document.getElementById('graf3d-legend');
if (!canvas || !container) return;
const W = container.clientWidth, H = container.clientHeight;
// Cleanup previous
if (window._sportGraf3D) {
window._sportGraf3D.renderer.dispose();
cancelAnimationFrame(window._sportGraf3D.raf);
}
const renderer = new THREE.WebGLRenderer({ canvas, antialias:true, alpha:true });
renderer.setSize(W, H);
renderer.setPixelRatio(Math.min(window.devicePixelRatio,2));
renderer.setClearColor(0x080912, 1);
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x080912, 0.0015);
const camera = new THREE.PerspectiveCamera(60, W/H, 0.1, 2000);
camera.position.set(0, 120, 280);
camera.lookAt(0,0,0);
// Ambient + directional light
scene.add(new THREE.AmbientLight(0x111122, 2));
const dlight = new THREE.DirectionalLight(0x4466ff, 1);
dlight.position.set(100, 200, 100);
scene.add(dlight);
const plight = new THREE.PointLight(0xf59e0b, 2, 300);
plight.position.set(0,50,0);
scene.add(plight);
// Star field
const starGeo = new THREE.BufferGeometry();
const starPos = [];
for (let i=0; i<2000; i++) {
starPos.push((Math.random()-0.5)*2000,(Math.random()-0.5)*2000,(Math.random()-0.5)*2000);
}
starGeo.setAttribute('position', new THREE.Float32BufferAttribute(starPos,3));
scene.add(new THREE.Points(starGeo, new THREE.PointsMaterial({color:0x334155,size:0.8})));
// Node meshes
const nodeMeshes = [];
const nodeIndex = {};
const typeGeo = {
root: new THREE.SphereGeometry(2.8, 32, 32),
savez: new THREE.SphereGeometry(1.6, 20, 20),
sport_cat: new THREE.OctahedronGeometry(1.8, 0),
grad: new THREE.DodecahedronGeometry(1.4, 0),
klub: new THREE.SphereGeometry(0.75, 12, 12),
};
nodes.forEach((n, i) => {
const geo = typeGeo[n.type] || typeGeo.klub;
const mat = new THREE.MeshPhongMaterial({
color: n.color, emissive: n.color, emissiveIntensity: 0.3,
shininess: 80, transparent: true, opacity: 0.9
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(n.x||0, n.y||0, n.z||0);
mesh.userData = { node: n, idx: i };
scene.add(mesh);
nodeMeshes.push(mesh);
nodeIndex[n.id] = mesh;
});
// Edge lines
const edgeMat = new THREE.LineBasicMaterial({ vertexColors: true, transparent: true, opacity: 0.25 });
const edgeGeo = new THREE.BufferGeometry();
const edgePos = [], edgeColors = [];
edges.forEach(e => {
const fm = nodeIndex[e.from], tm = nodeIndex[e.to];
if (!fm || !tm) return;
const c = new THREE.Color(e.color||0x334155);
edgePos.push(fm.position.x,fm.position.y,fm.position.z);
edgePos.push(tm.position.x,tm.position.y,tm.position.z);
edgeColors.push(c.r,c.g,c.b, c.r,c.g,c.b);
});
edgeGeo.setAttribute('position', new THREE.Float32BufferAttribute(edgePos,3));
edgeGeo.setAttribute('color', new THREE.Float32BufferAttribute(edgeColors,3));
scene.add(new THREE.LineSegments(edgeGeo, edgeMat));
// Labels (sprites)
function makeLabel(text, color) {
const cv = document.createElement('canvas');
cv.width=256; cv.height=48;
const ctx=cv.getContext('2d');
ctx.font='bold 20px system-ui';
ctx.fillStyle = '#'+color.toString(16).padStart(6,'0');
ctx.fillText(text, 4, 32);
const tex = new THREE.CanvasTexture(cv);
const mat = new THREE.SpriteMaterial({map:tex,transparent:true,opacity:0.85});
const s = new THREE.Sprite(mat);
s.scale.set(30,6,1);
return s;
}
// Only label large nodes
nodes.filter(n=>n.type!=='klub').forEach(n => {
const lbl = makeLabel(n.label, n.color);
lbl.position.set(n.x||0, (n.y||0)+(n.size||1)*3+4, n.z||0);
scene.add(lbl);
});
// Legend
const legendItems = {
'savez': { color:'#3b82f6', label:'Savez' },
'klub': { color:'#22c55e', label:'Klub' },
'★ qual':{ color:'#f59e0b', label:'Nositelj kvalitete' },
};
if (legend) {
legend.innerHTML = Object.entries(legendItems).map(([k,v])=>
`<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px">
<div style="width:10px;height:10px;border-radius:50%;background:${v.color}"></div>
<span>${v.label}</span></div>`
).join('');
}
// Camera orbit controls (simple)
let isDragging=false, prevMouse={x:0,y:0};
let theta=0, phi=0.4, radius=280;
canvas.addEventListener('mousedown', e=>{ isDragging=true; prevMouse={x:e.clientX,y:e.clientY}; });
canvas.addEventListener('mouseup', ()=>isDragging=false);
canvas.addEventListener('mousemove', e=>{
if (!isDragging) {
// Tooltip
const rect=canvas.getBoundingClientRect();
const mx=((e.clientX-rect.left)/rect.width)*2-1;
const my=-((e.clientY-rect.top)/rect.height)*2+1;
const raycaster=new THREE.Raycaster();
raycaster.setFromCamera({x:mx,y:my},camera);
const hits=raycaster.intersectObjects(nodeMeshes);
if (hits.length && hits[0].object.userData.node) {
const n=hits[0].object.userData.node;
tooltip.style.display='block';
tooltip.style.left=(e.clientX-rect.left+16)+'px';
tooltip.style.top=(e.clientY-rect.top-40)+'px';
const d=n.data||{};
tooltip.innerHTML = `<div style="font-weight:700;color:#f59e0b;margin-bottom:4px">${n.label}</div>`+
`<div style="color:#64748b;font-size:10px;text-transform:uppercase;margin-bottom:6px">${n.type}</div>`+
(d.sport?`<div>Sport: <b>${d.sport}</b></div>`:'')+(d.grad?`<div>Grad: <b>${d.grad}</b></div>`:'')+
(d.broj_clanova?`<div>Članova: <b>${d.broj_clanova}</b></div>`:'')+
(d.nositelj_kvalitete?`<div style="color:#f59e0b">★ Nositelj kvalitete</div>`:'');
canvas.style.cursor='pointer';
} else {
tooltip.style.display='none';
canvas.style.cursor='grab';
}
return;
}
const dx=e.clientX-prevMouse.x, dy=e.clientY-prevMouse.y;
theta -= dx * 0.008;
phi = Math.max(0.1, Math.min(Math.PI-0.1, phi - dy*0.006));
prevMouse={x:e.clientX,y:e.clientY};
});
canvas.addEventListener('click', e => {
const rect=canvas.getBoundingClientRect();
const mx=((e.clientX-rect.left)/rect.width)*2-1;
const my=-((e.clientY-rect.top)/rect.height)*2+1;
const raycaster=new THREE.Raycaster();
raycaster.setFromCamera({x:mx,y:my},camera);
const hits=raycaster.intersectObjects(nodeMeshes);
if (hits.length) {
const n=hits[0].object.userData.node;
if (n.type==='klub' && n.data?.id) {
navigate('klubovi');
setTimeout(()=>{ window._drillClubId = n.data.id; window._drillClubName=n.label; }, 300);
}
}
});
canvas.addEventListener('wheel', e=>{ radius=Math.max(80,Math.min(600,radius+e.deltaY*0.3)); });
canvas.style.cursor='grab';
// Animation loop
let t=0;
function animate() {
const raf=requestAnimationFrame(animate);
window._sportGraf3D = { renderer, raf };
t+=0.005;
// Auto-rotate
if (!isDragging) theta += 0.002;
camera.position.x = radius * Math.sin(phi) * Math.sin(theta);
camera.position.y = radius * Math.cos(phi);
camera.position.z = radius * Math.sin(phi) * Math.cos(theta);
camera.lookAt(0,0,0);
// Pulse root node
nodeMeshes[0] && (nodeMeshes[0].material.emissiveIntensity = 0.3 + Math.sin(t*3)*0.2);
renderer.render(scene, camera);
}
animate();
window._sportGraf3D = { renderer, raf: 0 };
}
async function pageSavezi() {
setTopbar('Organizacija', 'Županijski savezi');
const c = document.getElementById('content');
const q = document.getElementById('q-savezi')?.value || '';
try {
const razina = state.filters.savez_razina !== undefined ? state.filters.savez_razina : 'zupanijski'; const d = await api('/api/savezi?'+(q?`q=${encodeURIComponent(q)}&`:'')+(razina?`razina=${encodeURIComponent(razina)}`:'')+getSort('savezi'));
c.innerHTML = `
<div class="toolbar">
<input class="inp flex" id="q-savezi" placeholder="🔍 Pretraga..." value="${q}" oninput="dbS()">
<select onchange="state.filters.savez_razina=this.value;render()">
<option value="zupanijski" ${(state.filters.savez_razina||'zupanijski')==='zupanijski'?'selected':''}>Županijski (PGŽ)</option>
<option value="gradski" ${state.filters.savez_razina==='gradski'?'selected':''}>Gradski</option>
<option value="opcinski" ${state.filters.savez_razina==='opcinski'?'selected':''}>Općinski</option>
<option value="strukovni" ${state.filters.savez_razina==='strukovni'?'selected':''}>Strukovni</option>
<option value="nacional" ${state.filters.savez_razina==='nacional'?'selected':''}>Nacionalni</option>
<option value="" ${state.filters.savez_razina===''?'selected':''}>Sve razine</option>
</select>
<span style="color:var(--text-3);font-size:11px">${d.count} saveza</span>
</div>
<div class="tbl-wrap"><table>
<thead><tr>${tableHeader([{key:'naziv',label:'Naziv'},{key:'sport',label:'Sport'},{key:'godina',label:'Osn.'},{key:'klubova',label:'Klub.'},{key:'klubova',label:'Reg.',sort:false},{key:'klubova',label:'Tren.',sort:false},{key:'klubova',label:'Repr.',sort:false}], 'savezi')}</tr></thead>
<tbody>${d.rows.map(r=>`<tr onclick="showSavez(${r.id})">
<td><b>${r.naziv}</b></td>
<td class="dim">${r.sport||''}</td>
<td class="num">${r.godina_osnutka||''}</td>
<td class="num">${r.broj_klubova||0}</td>
<td class="num">${fmt(r.reg_2024)}</td>
<td class="num">${fmt(r.treneri_2024)}</td>
<td class="num">${fmt(r.repr_2024)}</td>
</tr>`).join('')}</tbody>
</table></div>
`;
} catch(e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
}
const dbS = debounce(pageSavezi, 300);
const dbStat = debounce(pageStatistika, 300);
async function showSavez(id) {
try {
const d = await api('/api/savezi/'+id);
openDrawer(`<div class="dr-h">
<div><div class="bc">SAVEZ · #${d.id}</div><h3>${d.naziv}</h3>
<div style="color:var(--text-3);font-size:12px;margin-top:3px">${d.sport||''} · osn. ${d.godina_osnutka||''}</div></div>
<button class="dr-x" onclick="closeDrawer()">✕</button></div>
<div class="dr-b"><dl>
<dt>Email</dt><dd>${d.email||''}</dd>
<dt>Web</dt><dd>${d.web?`<a href="${d.web}" target="_blank">${d.web}</a>`:''}</dd>
<dt>Klubova</dt><dd>${d.klubovi.length}</dd>
<dt>Manifestacija</dt><dd>${d.manifestacije.length}</dd>
</dl>
<h4>Statistika kroz godine</h4>
<table class="sub-tbl"><thead><tr><th>God</th><th style="text-align:right">Klub.</th><th style="text-align:right">Reg.</th><th style="text-align:right">Tren.</th><th style="text-align:right">Repr.</th></tr></thead>
<tbody>${d.statistika.map(s=>`<tr><td><b>${s.godina}</b></td><td class="num">${s.klubova_clanica}</td><td class="num">${s.registriranih}</td><td class="num">${s.trenera}</td><td class="num">${s.reprezentativaca}</td></tr>`).join('')}</tbody></table>
${d.klubovi.length?`<h4>Klubovi-članice (${d.klubovi.length})</h4>
<table class="sub-tbl"><tbody>${d.klubovi.slice(0,15).map(k=>{const mq=encodeURIComponent((k.naziv||'')+' '+(k.grad||'')+' Hrvatska');return `<tr style="cursor:pointer" onclick="closeDrawer();showKlub(${k.id})"><td><b style="color:var(--accent)">${k.naziv}</b></td><td><span class="bdg muted">${k.razina||''}</span></td><td class="dim" style="text-align:right"><a href="https://www.google.com/maps/search/?api=1&query=${mq}" target="_blank" onclick="event.stopPropagation()" class="muted">📍 ${k.grad||''}</a></td></tr>`}).join('')}</tbody></table>`:''}
</div>`);
} catch(e) { alert(e.message); }
}
async function pageKlubovi() {
setTopbar('Klubovi ERP', 'Upravljanje klubovima · '+document.getElementById('q-klub')?.value||'');
const c = document.getElementById('content');
const q = document.getElementById('q-klub')?.value||'';
const fnos = state.filters.nositelj||'';
const freg = state.filters.region||'';
const fsport = state.filters.sport||'';
const faktivan = state.filters.aktivan||'true';
try {
const [d, filterOpts] = await Promise.all([
api('/api/klubovi?' + new URLSearchParams(Object.fromEntries([
q?['q',q]:null, fnos?['nositelj',fnos]:null,
freg?['region',freg]:null, fsport?['sport',fsport]:null,
faktivan?['aktivan',faktivan]:null
].filter(Boolean))).toString() + getSort('klubovi')),
api('/api/v2/analytics/filter-options').catch(()=>({}))
]);
const sportOpts = (filterOpts.sportovi||['nogomet','košarka','odbojka','rukomet','bočanje','skijanje','jedrenje','atletika','tenis','šah','plivanje','veslanje']).slice(0,20);
c.innerHTML = `
<div class="toolbar">
<input class="inp flex" id="q-klub" placeholder="🔍 Pretraga..." value="${q}" oninput="dbK()">
<select onchange="state.filters.sport=(this.value||\"\");pageKlubovi()\"><option value=\"\">Svi sportovi</option><option value=\"nogomet\">⚽ Nogomet</option><option value=\"košarka\">🏀 Košarka</option><option value=\"odbojka\">🏐 Odbojka</option><option value=\"rukomet\">🤾 Rukomet</option><option value=\"bočanje\">🎯 Bočanje</option><option value=\"skijanje\">⛷ Skijanje</option><option value=\"tenis\">🎾 Tenis</option><option value=\"vaterpolo\">🤽 Vaterpolo</option><option value=\"atletika\">🏃 Atletika</option><option value=\"jedrenje\">⛵ Jedrenje</option></select>\n <button onclick=\"exportKlubovi()\" style=\"background:#1e293b;color:#94a3b8;border:1px solid #334;padding:5px 10px;border-radius:6px;cursor:pointer;font-size:11px\">↓ CSV</button>\n <select onchange=\"state.filters.nositelj=this.value;render()">
<option value="">Svi klubovi</option>
<option value="true" ${fnos==='true'?'selected':''}>⭐ Nositelji</option>
<option value="false" ${fnos==='false'?'selected':''}>Bez nositelja</option>
</select>
<select onchange="state.filters.region=this.value;render()">
<option value="">Sve regije</option>
${['Rijeka','Liburnija','Primorje','Gorski kotar','Otoci','Zaleđe'].map(r=>`<option value="${r}" ${freg===r?'selected':''}>${r}</option>`).join('')}
</select>
</div>
<div class="tbl-wrap"><table>
<thead><tr>${tableHeader([{key:'klub',label:'Klub'},{key:'sport',label:'Sport'},{key:'grad',label:'Grad',sort:false},{key:'predsjednik',label:'Predsjednik',sort:false},{key:'oib',label:'OIB',sort:false},{key:'broj_clanova',label:'Čl.'},{key:'completeness',label:'Kompletnost',sort:false},{key:'akcije',label:'',sort:false}], 'klubovi')}</tr></thead>
<tbody>${d.rows.map(r=>{
const enrichDots = [
r.ima_oib ? '<span title="OIB" style="color:var(--ok)">●</span>' : '<span title="Nema OIB" style="color:var(--text-dim);opacity:0.3">●</span>',
r.ima_predsjednika ? '<span title="Predsjednik" style="color:var(--ok)">●</span>' : '<span title="Nema predsjednika" style="color:var(--text-dim);opacity:0.3">●</span>',
r.ima_ciljeve ? '<span title="Ciljevi" style="color:var(--accent)">●</span>' : '<span title="Bez ciljeva" style="color:var(--text-dim);opacity:0.3">●</span>',
r.ima_sjediste ? '<span title="Sjedište" style="color:var(--accent)">●</span>' : '<span title="Bez sjedišta" style="color:var(--text-dim);opacity:0.3">●</span>',
].join(' ');
return `<tr onclick="showKlub(${r.id})">
<td><b>${r.klub}</b>${r.razina?` <span class="bdg muted" style="font-size:9px">${r.razina}</span>`:''}</td>
<td class="dim">${r.sport||''}</td>
<td>${r.predsjednik||'<span class="dim"></span>'}</td>
<td class="mono" style="font-size:11px">${r.oib||'<span class="dim"></span>'}</td>
<td class="dim" style="font-size:11px">${r.savez||''}</td>
<td class="num">${r.broj_clanova||0}</td>
<td style="white-space:nowrap;font-size:14px">${enrichDots}${r.nositelj_kvalitete?' <span class="bdg gold" style="font-size:9px">⭐</span>':''}</td>
</tr>`;
}).join('')}</tbody>
</table></div>
<div style="font-size:10px;color:var(--text-dim);margin-top:8px;padding:0 8px">
Status: <span style="color:var(--ok)">●</span> OIB · <span style="color:var(--ok)">●</span> predsjednik · <span style="color:var(--accent)">●</span> ciljevi · <span style="color:var(--accent)">●</span> sjedište
</div>
`;
} catch(e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
}
// CSV export za klubove
function exportKlubovi() {
api('/api/klubovi?limit=5000').then(d => {
const rows = d.rows || d || [];
const headers = ['ID','Naziv','Sport','Grad','OIB','Predsjednik','Email','Web','Broj_clanova','Nositelj','Aktivan'];
const csv = [headers.join(','), ...rows.map(r => [
r.id, '"'+(r.klub||'').replace(/"/g,'""')+'"', r.sport||'', r.grad||'',
r.oib||'', '"'+(r.predsjednik||'').replace(/"/g,'""')+'"',
r.email||'', r.web||'', r.broj_clanova||0,
r.nositelj_kvalitete?'DA':'NE', r.aktivan?'DA':'NE'
].join(','))].join('
');
const a = document.createElement('a');
a.href = 'data:text/csv;charset=utf-8,' + encodeURIComponent(csv);
a.download = 'pgz_klubovi_' + new Date().toISOString().slice(0,10) + '.csv';
a.click();
});
}
// Data quality dashboard
async function pageDataQuality() {
setTopbar('Kvaliteta podataka', 'Kompletnost i enrichment status');
const c = document.getElementById('content');
c.innerHTML = '<div class="loader">Analiziram podatke...</div>';
const [quality, gap] = await Promise.all([
api('/api/v2/audit/data-quality').catch(()=>({})),
api('/api/v2/pgz/enrichment-gap').catch(()=>({rows:[]}))
]);
const fields = quality.fields || [];
const totalKlubova = quality.total_klubova || 0;
c.innerHTML = `
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;margin-bottom:20px">
${(fields||[]).map(f=>{
const pct=Math.round((f.has||0)/(totalKlubova||1)*100);
const col=pct>=80?'#22c55e':pct>=50?'#f59e0b':'#ef4444';
return `<div style="background:#0d1117;border:1px solid #1e293b;border-radius:10px;padding:14px">
<div style="font-size:11px;color:#64748b;margin-bottom:6px;text-transform:uppercase">${f.field||f.label}</div>
<div style="font-size:24px;font-weight:700;color:${col}">${pct}%</div>
<div style="margin-top:8px;background:#1e293b;height:4px;border-radius:2px">
<div style="width:${pct}%;height:100%;background:${col};border-radius:2px"></div>
</div>
<div style="font-size:10px;color:#475569;margin-top:4px">${f.has||0} / ${totalKlubova} klubova</div>
</div>`;
}).join('')}
</div>
<h3 style="color:#94a3b8;margin-bottom:12px">Klubovi bez ključnih podataka</h3>
<div class="tbl-wrap"><table>
<thead><tr><th>Klub</th><th>Sport</th><th>Grad</th><th>Nedostaje</th><th>Akcija</th></tr></thead>
<tbody>${((gap.rows||[]).slice(0,50)).map(r=>`
<tr onclick="showKlub(${r.id})" style="cursor:pointer">
<td><b>${r.naziv||r.klub}</b></td>
<td class="dim">${r.sport||''}</td>
<td class="dim">${r.grad||''}</td>
<td style="color:#ef4444;font-size:11px">${r.nedostaje||r.missing_fields||''}</td>
<td><button onclick="event.stopPropagation();enrichKlub(${r.id})" style="background:#0f172a;border:1px solid #f59e0b22;color:#f59e0b;padding:2px 8px;border-radius:4px;cursor:pointer;font-size:10px">⚡ AI Enrichment</button></td>
</tr>`).join('')}
</tbody>
</table></div>`;
}
async function enrichKlub(id) {
const r = await api('/api/v2/enrich/klub-web?klub_id='+id).catch(e=>({error:e.message}));
if (r.error) alert('Enrichment: ' + r.error);
else { showKlub(id); }
}
// DMS — Document Management
async function pageDMS() {
setTopbar('DMS', 'Upravljanje dokumentima · OCR · Pretraga');
const c = document.getElementById('content');
const q = document.getElementById('dms-q')?.value||'';
const [docs, stats] = await Promise.all([
api('/api/v2/dokumenti/list?' + (q?'q='+encodeURIComponent(q):'')).catch(()=>({rows:[]})),
api('/api/v2/audit/coverage').catch(()=>({}))
]);
c.innerHTML = `
<div class="toolbar" style="margin-bottom:16px">
<input id="dms-q" class="inp" placeholder="🔍 Pretraži dokumente (full-text)..." value="${q}" oninput="dbDMS()" style="flex:1;max-width:400px">
<label style="background:#1e40af;color:#fff;padding:6px 14px;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600">
⬆ Upload dokument
<input type="file" style="display:none" onchange="uploadDok(this)" accept=".pdf,.doc,.docx,.jpg,.png">
</label>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:16px">
<div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px">
<div style="color:#64748b;font-size:10px;text-transform:uppercase">Ukupno dokumenata</div>
<div style="font-size:28px;font-weight:700;color:#e2e8f0">${stats.total_docs||''}</div>
</div>
<div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px">
<div style="color:#64748b;font-size:10px;text-transform:uppercase">OCR obrađeno</div>
<div style="font-size:28px;font-weight:700;color:#22c55e">${stats.ocr_done||''}</div>
</div>
<div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px">
<div style="color:#64748b;font-size:10px;text-transform:uppercase">Čeka OCR</div>
<div style="font-size:28px;font-weight:700;color:#f59e0b">${stats.ocr_pending||''}</div>
</div>
</div>
<div class="tbl-wrap"><table>
<thead><tr><th>Dokument</th><th>Tip</th><th>Klub</th><th>Datum</th><th>OCR</th><th>Akcije</th></tr></thead>
<tbody>${((docs.rows||docs||[]).slice(0,100)).map(d=>`
<tr>
<td><b style="color:#e2e8f0">${d.naziv||d.title||d.filename||''}</b></td>
<td><span class="bdg muted" style="font-size:10px">${d.tip||d.type||d.razina||''}</span></td>
<td class="dim" style="font-size:11px">${d.klub_naziv||d.klub||''}</td>
<td class="dim" style="font-size:11px">${d.datum||(d.created_at||'').slice(0,10)||''}</td>
<td style="text-align:center">${d.has_text||d.ocr_done?'<span style="color:#22c55e">✓</span>':'<span style="color:#334155">○</span>'}</td>
<td style="white-space:nowrap">
<button onclick="viewDok(${d.id||d.did})" style="background:#0f172a;border:1px solid #1e293b;color:#64748b;padding:2px 8px;border-radius:4px;cursor:pointer;font-size:10px;margin-right:4px">👁 Pregled</button>
${!d.has_text?`<button onclick="ocrDok(${d.id||d.did})" style="background:#0f172a;border:1px solid #f59e0b22;color:#f59e0b;padding:2px 8px;border-radius:4px;cursor:pointer;font-size:10px">🔤 OCR</button>`:''}
</td>
</tr>`).join('')}
</tbody>
</table></div>`;
}
const dbDMS = debounce(pageDMS, 350);
async function viewDok(id) {
const d = await api('/api/v2/dokumenti/'+id).catch(()=>null);
if (!d) return;
const modal = document.createElement('div');
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.85);z-index:1000;display:flex;align-items:center;justify-content:center';
modal.innerHTML = `<div style="background:#0d1117;border:1px solid #1e293b;border-radius:12px;padding:24px;max-width:700px;width:90%;max-height:80vh;overflow-y:auto">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<h2 style="color:#e2e8f0;font-size:16px;margin:0">${d.naziv||d.title||'Dokument'}</h2>
<button onclick="this.closest('[style*=fixed]').remove()" style="background:none;border:none;color:#64748b;cursor:pointer;font-size:20px">×</button>
</div>
${d.text_content?`<pre style="color:#94a3b8;font-size:12px;white-space:pre-wrap;line-height:1.6">${d.text_content.slice(0,3000)}</pre>`:'<div style="color:#475569">Tekst nije dostupan. Pokrenite OCR.</div>'}
${d.pdf_url?`<a href="${d.pdf_url}" target="_blank" style="display:inline-block;margin-top:12px;color:#3b82f6;font-size:12px">↗ Otvori PDF</a>`:''}
</div>`;
modal.onclick = e => { if(e.target===modal) modal.remove(); };
document.body.appendChild(modal);
}
async function ocrDok(id) {
const btn = event.target;
btn.textContent = '⏳ OCR...'; btn.disabled=true;
const r = await api('/api/ai/ocr-prilog?doc_id='+id, {method:'POST'}).catch(e=>({error:e.message}));
if (r.error) { btn.textContent='⚠ Greška'; btn.style.color='#ef4444'; }
else { btn.textContent='✓ Gotovo'; btn.style.color='#22c55e'; }
}
async function uploadDok(input) {
const file = input.files[0]; if(!file) return;
const fd = new FormData(); fd.append('file', file);
const r = await fetch('/api/v2/dms/upload', {method:'POST', body:fd}).then(r=>r.json()).catch(e=>({error:e.message}));
if (r.error) alert('Upload greška: '+r.error);
else { pageDMS(); }
}
const dbK = debounce(pageKlubovi, 300);
async function showKlub(id) {
navigate('klub_detail_' + id);
const c = document.getElementById('content');
c.innerHTML = '<div class="loader">Učitavam klub...</div>';
try {
const [d, clanovi, docs, nat] = await Promise.all([
api('/api/v2/klub/' + id + '/dashboard'),
api('/api/v2/klubovi/' + id + '/clanovi').catch(()=>({rows:[]})),
api('/api/v2/dokumenti/list?klub_id=' + id).catch(()=>({rows:[]})),
api('/api/v2/klub/' + id + '/natjecanja').catch(()=>([]))
]);
const k = d.klub || {};
const stats = d.stats || {};
const clanoviArr = clanovi.rows || clanovi || [];
const docsArr = docs.rows || docs || [];
const natArr = Array.isArray(nat) ? nat : (nat.rows || []);
// Completeness calculation
const fields = ['oib','predsjednik','email','web','adresa','telefon','trener_glavni','iban','logo_url','opis_djelatnosti'];
const filled = fields.filter(f => k[f] && k[f] !== '').length;
const compPct = Math.round(filled / fields.length * 100);
const compCol = compPct >= 80 ? '#22c55e' : compPct >= 50 ? '#f59e0b' : '#ef4444';
c.innerHTML = `
<div style="display:flex;align-items:center;gap:16px;margin-bottom:20px;flex-wrap:wrap">
<div style="width:56px;height:56px;border-radius:50%;background:linear-gradient(135deg,#1e3a5f,#0f172a);border:2px solid #f59e0b33;display:flex;align-items:center;justify-content:center;font-size:20px;flex-shrink:0">
${k.logo_url ? `<img src="${k.logo_url}" style="width:100%;height:100%;border-radius:50%;object-fit:cover" onerror="this.parentElement.innerHTML='⚽'">` : '⚽'}
</div>
<div style="flex:1;min-width:200px">
<div style="font-size:20px;font-weight:700;color:#e2e8f0">${k.naziv||''}</div>
<div style="color:#64748b;font-size:12px;margin-top:2px">
${k.sport ? `<span class="bdg" style="background:#0f172a;margin-right:6px">${k.sport}</span>` : ''}
${k.grad ? `<span style="margin-right:8px">📍 ${k.grad}</span>` : ''}
${k.savez_naziv ? `<span>🏛 ${k.savez_naziv}</span>` : ''}
</div>
</div>
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
${k.nositelj_kvalitete ? '<span style="background:#f59e0b22;color:#f59e0b;border:1px solid #f59e0b44;padding:4px 10px;border-radius:20px;font-size:11px;font-weight:700">⭐ Nositelj kvalitete</span>' : ''}
<div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:8px 14px;text-align:center">
<div style="font-size:18px;font-weight:700;color:${compCol}">${compPct}%</div>
<div style="font-size:9px;color:#475569;text-transform:uppercase">Kompletnost</div>
</div>
<button onclick="navigate('klubovi')" style="background:#0f172a;border:1px solid #1e293b;color:#64748b;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:11px">← Natrag</button>
<button onclick="enrichKlub(${k.id})" style="background:#0f172a;border:1px solid #f59e0b44;color:#f59e0b;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:11px">⚡ AI Enrich</button>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:16px">
${[
['Predsjednik', k.predsjednik],
['Tajnik', k.tajnik],
['Trener', k.trener_glavni],
['Email', k.email ? `<a href="mailto:${k.email}" style="color:#3b82f6">${k.email}</a>` : null],
['Web', k.web ? `<a href="${k.web}" target="_blank" style="color:#3b82f6">${k.web}</a>` : null],
['Telefon', k.telefon],
['OIB', k.oib],
['IBAN', k.iban],
['Adresa', k.adresa || k.sjediste],
['Reg. broj', k.reg_broj],
['Osnovan', k.datum_osnivanja_full || k.godina_osnutka],
['Razina natj.', k.razina],
['Status', k.udruga_status],
['HOO savez', k.savez_naziv],
].filter(([,v])=>v).map(([l,v])=>`
<div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:10px 12px">
<div style="font-size:9px;color:#475569;text-transform:uppercase;margin-bottom:3px">${l}</div>
<div style="color:#e2e8f0;font-size:12px;font-weight:500">${v}</div>
</div>`).join('')}
</div>
${k.opis_djelatnosti ? `
<div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:14px;margin-bottom:14px">
<div style="font-size:10px;color:#475569;text-transform:uppercase;margin-bottom:6px">Opis djelatnosti</div>
<div style="color:#94a3b8;font-size:12px;line-height:1.7">${k.opis_djelatnosti.slice(0,600)}${k.opis_djelatnosti.length>600?'…':''}</div>
</div>` : ''}
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:14px">
<div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px;text-align:center">
<div style="font-size:26px;font-weight:700;color:#3b82f6">${clanoviArr.length || k.broj_clanova || 0}</div>
<div style="font-size:10px;color:#475569">Članova</div>
</div>
<div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px;text-align:center">
<div style="font-size:26px;font-weight:700;color:#22c55e">${docsArr.length}</div>
<div style="font-size:10px;color:#475569">Dokumenata</div>
</div>
<div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px;text-align:center">
<div style="font-size:26px;font-weight:700;color:#f59e0b">${natArr.length}</div>
<div style="font-size:10px;color:#475569">Natjecanja</div>
</div>
</div>
${clanoviArr.length ? `
<h3 style="color:#94a3b8;margin:16px 0 10px;font-size:13px;text-transform:uppercase;letter-spacing:1px">Članovi (${clanoviArr.length})</h3>
<div class="tbl-wrap"><table>
<thead><tr><th>Ime i prezime</th><th>Kategorija</th><th>Pozicija</th><th>Licenca vrijedi</th><th>Status</th></tr></thead>
<tbody>${clanoviArr.slice(0,50).map(c=>{
const licons = [];
if(c.reprezentativac) licons.push('<span title="Reprezentativac" style="color:#f59e0b">🏅</span>');
if(c.kategoriziran) licons.push('<span title="Kategoriziran HOO" style="color:#3b82f6">⭐</span>');
if(c.stipendiran) licons.push('<span title="Stipendiran" style="color:#22c55e">💰</span>');
return `<tr onclick="navigate('sportas_${c.id}')" style="cursor:pointer">
<td><b>${c.prezime||''} ${c.ime||''}</b></td>
<td class="dim" style="font-size:11px">${c.hoo_kategorija||c.kategorija||''}</td>
<td class="dim" style="font-size:11px">${c.pozicija||c.uloga||''}</td>
<td class="dim mono" style="font-size:10px">${c.licenca_vrijedi_do||''}</td>
<td style="white-space:nowrap">${licons.join('')} ${c.aktivan?'<span style="color:#22c55e;font-size:10px">●</span>':'<span style="color:#334155;font-size:10px">●</span>'}</td>
</tr>`;
}).join('')}
</tbody>
</table></div>` : '<div style="color:#334155;padding:16px;text-align:center;font-size:12px">Nema registriranih članova</div>'}
${docsArr.length ? `
<h3 style="color:#94a3b8;margin:16px 0 10px;font-size:13px;text-transform:uppercase;letter-spacing:1px">Dokumenti</h3>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px">
${docsArr.slice(0,12).map(d=>`
<div onclick="viewDok(${d.id||d.did})" style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:10px 12px;cursor:pointer" onmouseover="this.style.borderColor='#f59e0b44'" onmouseout="this.style.borderColor='#1e293b'">
<div style="font-size:20px;margin-bottom:4px">${(d.tip||d.type||'').includes('pdf')?'📄':'📋'}</div>
<div style="font-size:11px;color:#e2e8f0;font-weight:500">${(d.naziv||d.title||'Dokument').slice(0,40)}</div>
<div style="font-size:10px;color:#475569;margin-top:3px">${d.tip||d.razina||''} · ${(d.created_at||'').slice(0,10)||''}</div>
</div>`).join('')}
</div>` : ''}
`;
} catch(e) { c.innerHTML = `<div class="ban crit">Greška: ${e.message}</div>`; }
}
async function pageClanovi() {
setTopbar('Organizacija', 'Članovi');
const c = document.getElementById('content');
const q = document.getElementById('q-clan')?.value||'';
const fkat = state.filters.kategorija||'';
const fspol = state.filters.spol||'';
const fr = state.filters.repr||'';
try {
const d = await api('/api/clanovi?'+(q?`q=${encodeURIComponent(q)}`:'')+(fkat?`&kategorija=${fkat}`:'')+(fspol?`&spol=${fspol}`:'')+(fr?`&reprezentativac=${fr}`:'')+getSort('clanovi'));
c.innerHTML = `
<div class="toolbar">
<input class="inp flex" id="q-clan" placeholder="🔍 Ime, OIB..." value="${q}" oninput="dbC()">
<select onchange="state.filters.kategorija=this.value;render()">
<option value="">Sve</option>
${['registrirani','neregistrirani','rekreativac','trener'].map(k=>`<option value="${k}" ${fkat===k?'selected':''}>${k}</option>`).join('')}
</select>
<select onchange="state.filters.spol=this.value;render()">
<option value="">Svi</option>
<option value="M" ${fspol==='M'?'selected':''}>M</option>
<option value="Ž" ${fspol==='Ž'?'selected':''}>Ž</option>
</select>
</div>
${!state.isAdmin?`<div class="ban warn"><b>🔒</b> Privatni podaci zamagljeni · klikni VIEWER za admin pristup</div>`:''}
<div class="tbl-wrap"><table>
<thead><tr>${tableHeader([{key:'prezime',label:'Prezime'},{key:'ime',label:'Ime'},{key:'oib',label:'OIB'},{key:'klub',label:'Klub'},{key:'kategorija',label:'Kat.'},{key:'datum_rodenja',label:'Rod.'},{key:'oib',label:'Liječ. do',sort:false},{key:'oib',label:'Dug',sort:false}], 'clanovi')}</tr></thead>
<tbody>${d.rows.length===0?'<tr><td colspan="8" class="empty">Nema članova</td></tr>':
d.rows.map(r=>`<tr>
<td><b>${r.prezime}</b></td>
<td>${r.ime}</td>
<td class="mono dim">${r.oib||''}</td>
<td>${r.klub_naziv||''}</td>
<td>${r.kategorija?`<span class="bdg info">${r.kategorija}</span>`:''}</td>
<td class="dim">${fmtDate(r.datum_rodenja)}</td>
<td>${r.lijecnicki_vrijedi_do?(new Date(r.lijecnicki_vrijedi_do)<new Date()?`<span class="bdg crit">${fmtDate(r.lijecnicki_vrijedi_do)}</span>`:`<span class="bdg ok">${fmtDate(r.lijecnicki_vrijedi_do)}</span>`):''}</td>
<td class="num mono">${r.dug_clanarine?`<span style="color:var(--crit)">${fmtEur(r.dug_clanarine)}</span>`:''}</td>
</tr>`).join('')}</tbody>
</table></div>
`;
} catch(e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
}
const dbC = debounce(pageClanovi, 300);
async function pageClanarine() {
setTopbar('Financije', 'Članarine');
const c = document.getElementById('content');
const fg = state.filters.godina||''; const fs = state.filters.status||'';
try {
const d = await api('/api/clanarine?'+(fg?`godina=${fg}`:'')+(fs?`&status=${fs}`:'')+getSort('clanarine'));
c.innerHTML = `
<div class="grid g4">
<div class="card"><div class="stat-l">Ukupno</div><div class="stat-v">${d.count}</div></div>
<div class="card acc"><div class="stat-l">Propisano</div><div class="stat-v sm">${fmtEur(d.summary.total_propisan)}</div></div>
<div class="card ok"><div class="stat-l">Plaćeno</div><div class="stat-v sm">${fmtEur(d.summary.total_placen)}</div></div>
<div class="card crit"><div class="stat-l">Dug</div><div class="stat-v sm">${fmtEur(d.summary.total_dug)}</div></div>
</div>
<div class="toolbar" style="margin-top:14px">
<select onchange="state.filters.godina=this.value;render()">
<option value="">Sve godine</option>
${[2026,2025,2024,2023,2022].map(y=>`<option value="${y}" ${fg==y?'selected':''}>${y}</option>`).join('')}
</select>
<select onchange="state.filters.status=this.value;render()">
<option value="">Svi statusi</option>
<option value="podmireno" ${fs==='podmireno'?'selected':''}>✓ Podmireno</option>
<option value="djelomicno" ${fs==='djelomicno'?'selected':''}>~ Djelomično</option>
<option value="nepodmireno" ${fs==='nepodmireno'?'selected':''}>✗ Nepodmireno</option>
</select>
</div>
<div class="tbl-wrap"><table>
<thead><tr>${tableHeader([{key:'godina',label:'God.'},{key:'klub',label:'Klub'},{key:'iznos',label:'Propisano'},{key:'iznos',label:'Plaćeno',sort:false},{key:'iznos',label:'Dug',sort:false},{key:'datum_uplate',label:'Uplata'},{key:'status',label:'Status'}], 'clanarine')}</tr></thead>
<tbody>${d.rows.length===0?'<tr><td colspan="7" class="empty">Bez zapisa</td></tr>':
d.rows.map(r=>`<tr>
<td><b>${r.godina}</b></td>
<td>${r.klub||''}</td>
<td class="num mono">${fmtEur(r.iznos_propisan)}</td>
<td class="num mono" style="color:var(--ok)">${fmtEur(r.iznos_placen)}</td>
<td class="num mono" style="color:${r.dug>0?'var(--crit)':'var(--text-3)'}">${fmtEur(r.dug)}</td>
<td class="dim">${fmtDate(r.datum_uplate)}</td>
<td><span class="bdg ${r.status==='podmireno'?'ok':r.status==='djelomicno'?'warn':'crit'}">${r.status}</span></td>
</tr>`).join('')}</tbody>
</table></div>
`;
} catch(e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
}
async function pageLijecnicki() {
setTopbar('Zdravlje', 'Liječnički pregledi');
const c = document.getElementById('content');
const fs = state.filters.status||'';
try {
const d = await api('/api/lijecnicki?'+(fs?`status=${encodeURIComponent(fs)}`:'')+getSort('lijecnicki'));
c.innerHTML = `
<div class="grid g4">
<div class="card"><div class="stat-l">Ukupno</div><div class="stat-v">${d.count}</div></div>
<div class="card crit"><div class="stat-l">Istekli</div><div class="stat-v">${d.summary?.istekli||0}</div></div>
<div class="card warn"><div class="stat-l">Uskoro</div><div class="stat-v">${d.summary?.uskoro||0}</div></div>
<div class="card acc"><div class="stat-l">ZZJZ udio</div><div class="stat-v sm">${fmtEur(d.summary?.total_zzjz)}</div></div>
</div>
<div class="toolbar" style="margin-top:14px">
<select onchange="state.filters.status=this.value;render()">
<option value="">Svi</option>
<option value="Validan" ${fs==='Validan'?'selected':''}>✓ Validni</option>
<option value="Ističe uskoro" ${fs==='Ističe uskoro'?'selected':''}>~ Uskoro</option>
<option value="Istekao" ${fs==='Istekao'?'selected':''}>✗ Istekli</option>
</select>
</div>
<div class="tbl-wrap"><table>
<thead><tr>${tableHeader([{key:'clan',label:'Sportaš'},{key:'klub',label:'Klub'},{key:'datum_pregleda',label:'Datum'},{key:'vrijedi_do',label:'Vrijedi do'},{key:'iznos',label:'Iznos'},{key:'iznos',label:'ZZJZ',sort:false},{key:'iznos',label:'Status',sort:false}], 'lijecnicki')}</tr></thead>
<tbody>${d.rows.length===0?'<tr><td colspan="7" class="empty">Bez zapisa</td></tr>':
d.rows.map(r=>`<tr>
<td><b>${r.clan}</b></td>
<td>${r.klub||''}</td>
<td class="dim">${fmtDate(r.datum_pregleda)}</td>
<td>${fmtDate(r.vrijedi_do)}</td>
<td class="num mono">${fmtEur(r.iznos)}</td>
<td class="num mono" style="color:var(--ok)">${fmtEur(r.iznos_zzjz)}</td>
<td><span class="bdg ${r.status_pregled==='Validan'?'ok':r.status_pregled==='Ističe uskoro'?'warn':'crit'}">${r.status_pregled}</span></td>
</tr>`).join('')}</tbody>
</table></div>
`;
} catch(e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
}
async function pagePotpore() {
setTopbar('Financije', 'Potpore nositeljima kvalitete');
const c = document.getElementById('content');
const fg = state.filters.godina||'';
try {
const d = await api('/api/potpore?'+(fg?`godina=${fg}`:'')+getSort('potpore'));
const total = d.rows.reduce((s,r)=>s+parseFloat(r.iznos||0),0);
c.innerHTML = `
<div class="grid g3">
<div class="card ok"><div class="stat-l">Ukupno</div><div class="stat-v sm">${fmtEur(total)}</div></div>
<div class="card"><div class="stat-l">Klubova</div><div class="stat-v">${new Set(d.rows.map(r=>r.naziv_kluba)).size}</div></div>
<div class="card"><div class="stat-l">Zapisa</div><div class="stat-v">${d.count}</div></div>
</div>
<div class="toolbar" style="margin-top:14px">
<select onchange="state.filters.godina=this.value;render()">
<option value="">Sve godine</option>
${[2025,2024,2023,2022,2021].map(y=>`<option value="${y}" ${fg==y?'selected':''}>${y}</option>`).join('')}
</select>
</div>
<div class="tbl-wrap"><table>
<thead><tr>${tableHeader([{key:'klub',label:'Klub'},{key:'godina',label:'God.'},{key:'iznos',label:'Iznos'}], 'potpore')}</tr></thead>
<tbody>${d.rows.map(r=>`<tr>
<td><b>${r.naziv_kluba}</b></td>
<td>${r.godina}</td>
<td class="num mono">${fmtEur(r.iznos)}</td>
</tr>`).join('')}</tbody>
</table></div>
`;
} catch(e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
}
async function pageProracun() {
setTopbar('Financije', 'Proračun PGŽ za sport');
const c = document.getElementById('content');
try {
const d = await api('/api/proracun');
const max = Math.max(...d.rows.map(r=>parseFloat(r.ukupno||0)),1);
c.innerHTML = `
<div class="card">
<div class="ct">📈 Trend 2016—2026 <span class="meta">${d.count} godina</span></div>
${d.rows.map(r=>`<div class="bar">
<div class="l"><b>${r.godina}.</b></div>
<div class="t"><div class="f ok" style="width:${(parseFloat(r.ukupno)/max*100).toFixed(1)}%"></div></div>
<div class="v">${fmtEur(r.ukupno)}</div>
</div>`).join('')}
</div>
<div class="sect">Detaljna tablica</div>
<div class="tbl-wrap"><table>
<thead><tr><th>God.</th><th style="text-align:right">PGŽ</th><th style="text-align:right">Reb.1</th><th style="text-align:right">Reb.2</th><th style="text-align:right">PGŽ uk.</th><th style="text-align:right">Min.</th><th style="text-align:right">UKUPNO</th></tr></thead>
<tbody>${d.rows.map(r=>`<tr>
<td><b>${r.godina}</b></td>
<td class="num mono">${fmtEur(r.proracun_pgz)}</td>
<td class="num mono">${fmtEur(r.rebalans1)}</td>
<td class="num mono">${fmtEur(r.rebalans2)}</td>
<td class="num mono">${fmtEur(r.ukupno_pgz)}</td>
<td class="num mono">${fmtEur(r.ministarstvo)}</td>
<td class="num mono" style="color:var(--ok);font-weight:700">${fmtEur(r.ukupno)}</td>
</tr>`).join('')}</tbody>
</table></div>
`;
} catch(e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
}
async function pageStatistika() {
setTopbar('Operativa', 'Statistika saveza');
const c = document.getElementById('content');
const fg = state.filters.godina || '2026';
const fr = state.filters.stat_razina !== undefined ? state.filters.stat_razina : 'zupanijski';
const fq = state.filters.stat_q || '';
try {
const d = await api('/api/statistika?godina='+fg+(fr?`&razina=${encodeURIComponent(fr)}`:'')+(fq?`&q=${encodeURIComponent(fq)}`:'')+getSort('statistika'));
c.innerHTML = `
<div class="toolbar">
<input class="inp flex" id="q-stat" placeholder="Pretraga saveza..." value="${fq}" oninput="dbStat()">
<select onchange="state.filters.godina=this.value;render()" title="Godina">
${[2026,2024,2023,2022,2021,2020].map(y=>`<option value="${y}" ${fg==y?'selected':''}>${y}.</option>`).join('')}
</select>
<select onchange="state.filters.stat_razina=this.value;render()" title="Razina saveza">
<option value="zupanijski" ${fr==='zupanijski'?'selected':''}>Županijski (PGŽ)</option>
<option value="gradski" ${fr==='gradski'?'selected':''}>Gradski</option>
<option value="opcinski" ${fr==='opcinski'?'selected':''}>Općinski</option>
<option value="strukovni" ${fr==='strukovni'?'selected':''}>Strukovni</option>
<option value="nacional" ${fr==='nacional'?'selected':''}>Nacionalni</option>
<option value="" ${fr===''?'selected':''}>Sve</option>
</select>
<span style="color:var(--text-3);font-size:11px;margin-left:auto">${d.count} saveza</span>
</div>
<div class="tbl-wrap"><table>
<thead><tr>${tableHeader([{key:'savez',label:'Savez'},{key:'klubova',label:'Klub.'},{key:'registriranih',label:'Reg.'},{key:'klubova',label:'Nereg.',sort:false},{key:'klubova',label:'Rekr.',sort:false},{key:'trenera',label:'Tren.'},{key:'reprezentativaca',label:'Repr.'}], 'statistika')}</tr></thead>
<tbody>${d.rows.map(r=>`<tr>
<td><b>${r.savez}</b></td>
<td class="num">${r.klubova_clanica}</td>
<td class="num">${r.registriranih}</td>
<td class="num dim">${r.neregistriranih}</td>
<td class="num dim">${r.rekreativaca}</td>
<td class="num">${r.trenera}</td>
<td class="num">${r.reprezentativaca}</td>
</tr>`).join('')}</tbody>
</table></div>
`;
} catch(e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
}
async function pageManifestacije() {
setTopbar('Operativa', 'Manifestacije');
const c = document.getElementById('content');
const fr = state.filters.razina||'';
try {
const d = await api('/api/manifestacije?'+(fr?`razina=${encodeURIComponent(fr)}`:'')+getSort('manifestacije'));
c.innerHTML = `
<div class="toolbar">
<select onchange="state.filters.razina=this.value;render()">
<option value="">Sve razine</option>
${['Klupska','Regionalna','Državna','Međunarodna'].map(r=>`<option value="${r}" ${fr===r?'selected':''}>${r}</option>`).join('')}
</select>
<span style="color:var(--text-3);font-size:11px;margin-left:auto">${d.count} manifestacija</span>
</div>
<div class="tbl-wrap"><table>
<thead><tr>${tableHeader([{key:'naziv',label:'Naziv'},{key:'mjesto',label:'Mjesto'},{key:'savez',label:'Savez',sort:false},{key:'razina',label:'Razina'},{key:'godina_od',label:'Od g.'},{key:'mjesto',label:'Učesnici',sort:false}], 'manifestacije')}</tr></thead>
<tbody>${d.rows.map(r=>`<tr>
<td><b>${r.naziv}</b></td>
<td>${r.mjesto||''}</td>
<td class="dim">${r.savez_naziv||''}</td>
<td><span class="bdg ${r.razina==='Međunarodna'?'gold':r.razina==='Državna'?'info':r.razina==='Regionalna'?'warn':'muted'}">${r.razina||''}</span></td>
<td class="num">${r.godina_od||''}</td>
<td class="dim">${r.broj_ucesnika||''}</td>
</tr>`).join('')}</tbody>
</table></div>
`;
} catch(e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
}
async function pageAlertovi() {
setTopbar('Pregled', 'Alertovi');
const c = document.getElementById('content');
try {
const d = await api('/api/alertovi?rijeseno=false');
c.innerHTML = `
<div class="toolbar">
<button class="btn warn" onclick="scanA()">↻ Skeniraj</button>
<span style="color:var(--text-3);font-size:11px;margin-left:auto">${d.count} aktivnih</span>
</div>
${d.count===0?'<div class="empty"><div class="empty-i">✓</div>Nema aktivnih alerta</div>':`
<div class="tbl-wrap"><table>
<thead><tr><th>Razina</th><th>Tip</th><th>Poruka</th><th>Datum</th><th style="text-align:right">Iznos</th><th>Akcija</th></tr></thead>
<tbody>${d.rows.map(r=>`<tr>
<td><span class="bdg ${r.razina==='CRITICAL'?'crit':r.razina==='WARNING'?'warn':'info'}">${r.razina}</span></td>
<td class="dim">${r.tip}</td>
<td>${r.poruka}</td>
<td class="dim">${fmtDate(r.datum)}</td>
<td class="num mono">${r.iznos?fmtEur(r.iznos):''}</td>
<td><button class="btn sec sm" onclick="event.stopPropagation();rijesi(${r.id})">Riješi</button></td>
</tr>`).join('')}</tbody>
</table></div>`}
`;
} catch(e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
}
async function scanA() { await fetch(API+'/api/alertovi/scan',{method:'POST',headers:{'Authorization':'Bearer '+state.token}}); pageAlertovi(); }
async function rijesi(id) { await fetch(API+'/api/alertovi/'+id+'/rijesi',{method:'PUT',headers:{'Authorization':'Bearer '+state.token}}); pageAlertovi(); }
async function pageZzjz() {
setTopbar('Zdravlje', 'ZZJZ PGŽ — Sufinanciranje');
const c = document.getElementById('content');
try {
const d = await api('/api/zzjz/dogovor');
const stv = d.stvarno_stanje||{};
c.innerHTML = `
<div class="ban info"><div><b>${d.info}</b><br><span style="opacity:0.85">${d.model}</span></div></div>
<div class="grid g3">
<div class="card"><div class="stat-l">Sportaša potencijalnih</div><div class="stat-v">${fmt(d.godisnji_potencijal?.sportasa_potencijalno)}</div></div>
<div class="card acc"><div class="stat-l">Procijenjeni godišnji</div><div class="stat-v sm">${fmtEur(d.godisnji_potencijal?.godisnji_trosak_eur)}</div></div>
<div class="card ok"><div class="stat-l">Pregleda u sustavu</div><div class="stat-v">${stv.pregleda||0}</div></div>
</div>
<div class="grid g2" style="margin-top:14px">
<div class="card">
<div class="ct">⚕ Stvarna podjela troškova</div>
<div class="donut-w">
${donut([parseFloat(stv.zzjz_isplata||0), parseFloat(stv.klub_isplata||0), parseFloat(stv.clan_isplata||0)],
['ZZJZ','Klub','Član'], ['#2DD4BF','#4A9EFF','#D4A852'],
fmt(parseFloat(stv.ukupan_trosak||0)), 'EUR')}
<div class="lg">
<div class="it"><div class="sw" style="background:#2DD4BF"></div><span class="lname">ZZJZ</span><span class="lval">${fmtEur(stv.zzjz_isplata)}</span></div>
<div class="it"><div class="sw" style="background:#4A9EFF"></div><span class="lname">Klub</span><span class="lval">${fmtEur(stv.klub_isplata)}</span></div>
<div class="it"><div class="sw" style="background:#D4A852"></div><span class="lname">Član</span><span class="lval">${fmtEur(stv.clan_isplata)}</span></div>
</div>
</div>
</div>
<div class="card">
<div class="ct">📋 Predviđeni tijek</div>
<ol style="padding-left:18px;line-height:1.9;font-size:12.5px;color:var(--text-2)">
<li><b>Klub registrira</b> sportaša u sustav</li>
<li><b>Sportaš odlazi</b> na liječnički u ZZJZ PGŽ</li>
<li><b>Liječnik unosi</b> nalaz, datum, vrijedi do</li>
<li><b>Sustav izračunava</b> udio: ZZJZ ⟷ klub ⟷ član</li>
<li><b>ZZJZ izdaje</b> račun klubu/PGŽ-u</li>
<li><b>Auto-alert</b> kada pregled ističe (60d/30d/0d)</li>
</ol>
</div>
</div>
`;
} catch(e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
}
// === NAVIGACIJA: breadcrumbs, back, keyboard ===
window.navStack = window.navStack || [];
function navPush(page, params) {
// Push current state to history (max 20)
if (state.page) {
window.navStack.push({page: state.page, ...JSON.parse(JSON.stringify(state))});
if (window.navStack.length > 20) window.navStack.shift();
}
}
function navBack() {
if (window.navStack.length === 0) { goto('dashboard'); return; }
const prev = window.navStack.pop();
Object.assign(state, prev);
render();
}
// ESC = back
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && window.navStack.length > 0 && !document.querySelector('.modal,dialog[open]')) {
e.preventDefault();
navBack();
}
});
function breadcrumbs(items) {
// items = [{label, onclick}]
let h = '<div class="breadcrumbs" style="display:flex;align-items:center;gap:8px;font-size:12px;color:var(--text2);margin-bottom:12px;flex-wrap:wrap">';
if (window.navStack.length > 0) {
h += '<button onclick="navBack()" title="Esc" style="background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:4px 10px;border-radius:6px;cursor:pointer;font-size:12px">← Nazad</button>';
h += '<span style="color:var(--text3)">·</span>';
}
items.forEach((it, i) => {
if (i > 0) h += '<span style="color:var(--text3)"></span>';
if (it.onclick) {
h += `<a onclick="${it.onclick}" style="cursor:pointer;color:${i===items.length-1?'var(--text-bright)':'var(--accent)'}">${it.label}</a>`;
} else {
h += `<span style="color:${i===items.length-1?'var(--text-bright)':'var(--text2)'}">${it.label}</span>`;
}
});
h += '</div>';
return h;
}
// Prikaži vrijednost ili "NEDOSTAJE" jasno
function val(v, label) {
if (v === null || v === undefined || v === '') {
return `<span style="color:var(--text3);font-style:italic">— ${label||'nedostaje'} —</span>`;
}
return v;
}
function valWarn(v, type) {
// type='date'/'string' - vraća badge za nedostajuće s warning
if (v === null || v === undefined || v === '') {
return `<span class="risk-medium" style="font-size:9px;padding:2px 6px;border-radius:3px">PODATAK NEDOSTAJE</span>`;
}
return v;
}
const ULOGA_BADGE = {
'igrac': '<span style="background:rgba(59,130,246,0.15);color:#60a5fa;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">⚽ IGRAČ</span>',
'sportaš': '<span style="background:rgba(59,130,246,0.15);color:#60a5fa;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">🏃 SPORTAŠ</span>',
'sportas': '<span style="background:rgba(59,130,246,0.15);color:#60a5fa;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">🏃 SPORTAŠ</span>',
'predsjednik': '<span style="background:rgba(245,158,11,0.2);color:#f59e0b;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">👑 PREDSJEDNIK</span>',
'dopredsjednik': '<span style="background:rgba(245,158,11,0.15);color:#fbbf24;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">👑 DOPREDSJEDNIK</span>',
'tajnik': '<span style="background:rgba(168,85,247,0.15);color:#c084fc;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">📋 TAJNIK</span>',
'direktor': '<span style="background:rgba(168,85,247,0.2);color:#a855f7;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">📊 DIREKTOR</span>',
'trener': '<span style="background:rgba(34,197,94,0.15);color:#4ade80;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">📋 TRENER</span>',
'pomocni_trener': '<span style="background:rgba(34,197,94,0.12);color:#86efac;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">📋 POMOĆNI TRENER</span>',
'trener_vratara': '<span style="background:rgba(34,197,94,0.12);color:#86efac;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">🥅 TRENER VRATARA</span>',
'kondicioni_trener': '<span style="background:rgba(239,68,68,0.15);color:#f87171;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">💪 KONDICIONI</span>',
'fizioterapeut': '<span style="background:rgba(20,184,166,0.15);color:#5eead4;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">🩺 FIZIOTERAPEUT</span>',
'lijecnik': '<span style="background:rgba(244,63,94,0.15);color:#fb7185;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">⚕ LIJEČNIK</span>',
'sudac': '<span style="background:rgba(99,102,241,0.15);color:#818cf8;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">🟨 SUDAC</span>',
'član uprave': '<span style="background:rgba(245,158,11,0.1);color:#fcd34d;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">🪑 UPRAVA</span>',
'član nadzornog odbora': '<span style="background:rgba(245,158,11,0.1);color:#fcd34d;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">🔍 NO</span>',
'team_manager': '<span style="background:rgba(168,85,247,0.15);color:#c084fc;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">📊 MANAGER</span>',
'analiticar': '<span style="background:rgba(99,102,241,0.15);color:#818cf8;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">📈 ANALITIČAR</span>',
'video_analiticar': '<span style="background:rgba(99,102,241,0.15);color:#818cf8;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">🎥 VIDEO</span>',
'ostalo': '<span style="background:rgba(107,114,128,0.15);color:#9ca3af;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">— OSTALO</span>',
'': '<span style="background:rgba(107,114,128,0.1);color:#6b7280;padding:3px 8px;border-radius:4px;font-size:10px">—</span>',
'trener': '<span style="background:rgba(16,185,129,0.15);color:#10b981;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">📋 TRENER</span>',
'kondicioni_trener': '<span style="background:rgba(16,185,129,0.15);color:#10b981;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">💪 KONDICIONI TRENER</span>',
'direktor': '<span style="background:rgba(245,158,11,0.15);color:#f59e0b;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">📊 DIREKTOR</span>',
'predsjednik': '<span style="background:rgba(168,85,247,0.15);color:#a855f7;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">👑 PREDSJEDNIK</span>',
'tajnik': '<span style="background:rgba(168,85,247,0.15);color:#a855f7;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">📝 TAJNIK</span>',
'fizioterapeut': '<span style="background:rgba(236,72,153,0.15);color:#ec4899;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">⚕️ FIZIO</span>',
'lijecnik': '<span style="background:rgba(236,72,153,0.15);color:#ec4899;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">⚕️ LIJEČNIK</span>',
'sudac': '<span style="background:rgba(107,114,128,0.15);color:#9ca3af;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">🦮 SUDAC</span>',
'ostalo': '<span style="background:rgba(107,114,128,0.15);color:#9ca3af;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">▪ OSTALO</span>',
};
function ulogaBadge(u) { return ULOGA_BADGE[u] || (u ? '<span style="color:var(--text3);font-size:10px">'+u+'</span>' : ''); }
// Sport ikone (emoji) za pregled svih sportova
const SPORT_ICONS = {
'nogomet': '⚽', 'rukomet': '🤾', 'košarka': '🏀', 'kosarka': '🏀',
'vaterpolo': '🤽', 'odbojka': '🏐', 'tenis': '🎾', 'stolni tenis': '🏓',
'atletika': '🏃', 'plivanje': '🏊', 'biciklizam': '🚴', 'boks': '🥊',
'karate': '🥋', 'judo': '🥋', 'taekwondo': '🥋', 'kickboxing': '🥊',
'jedriličarstvo': '⛵', 'jedrilicarstvo': '⛵', 'skijanje': '⛷️',
'ribolov': '🎣', 'šah': '♟️', 'sah': '♟️', 'streljaštvo': '🎯',
'streličarstvo': '🏹', 'gimnastika': '🤸', 'ples': '💃', 'kuglanje': '🎳',
'pikado': '🎯', 'planinarstvo': '🏔️', 'konjički sport': '🐎',
'veslanje': '🚣', 'mačevanje': '🤺', 'hrvanje': '🤼', 'penjanje': '🧗',
'multisport': '🏆', 'rekreacija': '🎯', 'motosport': '🏎️',
'baseball': '⚾', 'softball': '⚾', 'golf': '⛳', 'hokej': '🏒',
'parasport': '♿', 'parasportski': '♿', 'paraolimpijski': '♿',
'lov': '🦌', 'kajakaštvo': '🛶', 'curling': '🥌', 'eSport': '🎮',
'borilački sport': '🥋', 'penjanje': '🧗', 'sport gluhih': '🤟',
'sport slijepih': '👁️‍🗨️', 'olimpijski': '🏅', 'kineziologija': '📚',
'medicina': '⚕️', 'školski sport': '🎒', 'općenito': '🏟️',
};
function sportIcon(s) { return SPORT_ICONS[(s||'').toLowerCase()] || '🏃'; }
// === GUI UPGRADE - sprint 5 ===
function showSportasiModal(sport, filter, title) {
const params = new URLSearchParams();
if (sport) params.set('sport', sport);
if (filter === 'reprezentativci') params.set('reprezentativac', 'true');
if (filter === 'kategorizirani') params.set('kategorija_min', '1');
if (filter === 'klubovi') {
// Klubovi modal handled separately
return showKlubModal(sport, title);
}
if (filter === 'savezi') {
return showSavezModal(sport, title);
}
fetch('/sport/api/v2/clanovi?' + params.toString() + '&limit=200')
.then(r => r.json()).then(d => {
const items = d.data || d || [];
let html = '<div class="ri-modal" id="riModal" onclick="if(event.target===this)closeRiModal()">';
html += '<div class="ri-modal-box" style="max-width:1100px">';
html += '<div class="ri-modal-h"><h3 style="margin:0">' + title + ' <span class="muted" style="font-size:12px">(' + items.length + ')</span></h3>';
html += '<button class="ri-icon-btn" onclick="closeRiModal()" title="Zatvori">' + iconX() + '</button></div>';
html += '<div class="ri-modal-body">';
html += '<table class="ri-tbl ri-sortable" data-table="modal-sportasi"><thead><tr>';
html += '<th>Foto</th>';
html += '<th class="ri-sort" data-key="ime">Ime i prezime</th>';
html += '<th class="ri-sort" data-key="klub_naziv">Klub</th>';
html += '<th class="ri-sort" data-key="sport">Sport</th>';
html += '<th class="ri-sort" data-key="uloga">Uloga</th>';
html += '<th class="ri-sort num" data-key="kategorija_hoo">HOO</th>';
html += '<th class="ri-sort" data-key="reprezentativac">Repr.</th>';
html += '<th>Akcije</th>';
html += '</tr></thead><tbody>';
items.forEach(s => {
const ime = (s.ime||'') + ' ' + (s.prezime||'');
html += '<tr style="cursor:pointer" onclick="closeRiModal();gotoSportas(' + s.id + ')">';
html += '<td>' + (s.slika_url ? '<img src="' + imgProxy(s.slika_url) + '" style="width:30px;height:30px;border-radius:50%;object-fit:cover"/>' : '<span style="color:var(--text3)">' + iconUser() + '</span>') + '</td>';
html += '<td><b>' + ime + '</b></td>';
html += '<td class="muted">' + (s.klub_naziv || '-') + '</td>';
html += '<td>' + (s.sport || '-') + '</td>';
html += '<td>' + (s.uloga || '-') + '</td>';
html += '<td class="num">' + (s.kategorija_hoo ? '<span class="ri-badge ri-badge-gold">' + 'I'.repeat(s.kategorija_hoo) + '</span>' : '-') + '</td>';
html += '<td>' + (s.reprezentativac ? '<span class="ri-badge ri-badge-blue">REPR</span>' : '-') + '</td>';
html += '<td onclick="event.stopPropagation()">';
html += '<button class="ri-icon-btn-sm" onclick="closeRiModal();gotoSportas(' + s.id + ')" title="View">' + iconEye() + '</button>';
html += '</td>';
html += '</tr>';
});
html += '</tbody></table>';
html += '</div></div></div>';
const div = document.createElement('div');
div.innerHTML = html;
document.body.appendChild(div.firstChild);
// Attach sort handlers
attachTableSort(document.querySelector('#riModal .ri-sortable'));
}).catch(e => alert('Greška: ' + e));
}
function showKlubModal(sport, title) {
fetch('/sport/api/v2/klubovi/sa-clanstvom?sport=' + encodeURIComponent(sport || '') + '&limit=300')
.then(r => r.json()).then(d => {
const items = (d.data || d || []).filter(k => !sport || (k.sport||'').toLowerCase() === sport.toLowerCase());
let html = '<div class="ri-modal" id="riModal" onclick="if(event.target===this)closeRiModal()">';
html += '<div class="ri-modal-box" style="max-width:1100px">';
html += '<div class="ri-modal-h"><h3 style="margin:0">' + title + ' <span class="muted" style="font-size:12px">(' + items.length + ')</span></h3>';
html += '<button class="ri-icon-btn" onclick="closeRiModal()">' + iconX() + '</button></div>';
html += '<div class="ri-modal-body">';
html += '<table class="ri-tbl ri-sortable" data-table="modal-klubovi"><thead><tr>';
html += '<th class="ri-sort" data-key="naziv">Klub</th>';
html += '<th class="ri-sort" data-key="sport">Sport</th>';
html += '<th class="ri-sort" data-key="razina">Razina</th>';
html += '<th class="ri-sort" data-key="grad">Grad</th>';
html += '<th class="ri-sort num" data-key="broj_clanova">Članova</th>';
html += '<th>Akcije</th>';
html += '</tr></thead><tbody>';
items.forEach(k => {
html += '<tr style="cursor:pointer" onclick="closeRiModal();gotoKlubRoster(' + k.id + ')">';
html += '<td><b>' + (k.naziv||'') + '</b></td>';
html += '<td>' + (k.sport || '-') + '</td>';
html += '<td>' + (k.razina || '-') + '</td>';
html += '<td class="muted">' + (k.grad || '-') + '</td>';
html += '<td class="num">' + (k.broj_clanova || 0) + '</td>';
html += '<td onclick="event.stopPropagation()">';
html += '<button class="ri-icon-btn-sm" onclick="closeRiModal();gotoKlubRoster(' + k.id + ')" title="View">' + iconEye() + '</button>';
html += '</td>';
html += '</tr>';
});
html += '</tbody></table>';
html += '</div></div></div>';
const div = document.createElement('div');
div.innerHTML = html;
document.body.appendChild(div.firstChild);
attachTableSort(document.querySelector('#riModal .ri-sortable'));
});
}
function showSavezModal(sport, title) {
fetch('/sport/api/v2/savezi?sport=' + encodeURIComponent(sport || ''))
.then(r => r.json()).then(d => {
const items = d.data || d || [];
let html = '<div class="ri-modal" id="riModal" onclick="if(event.target===this)closeRiModal()">';
html += '<div class="ri-modal-box" style="max-width:900px">';
html += '<div class="ri-modal-h"><h3>' + title + ' <span class="muted">(' + items.length + ')</span></h3>';
html += '<button class="ri-icon-btn" onclick="closeRiModal()">' + iconX() + '</button></div>';
html += '<div class="ri-modal-body"><table class="ri-tbl ri-sortable"><thead><tr>';
html += '<th class="ri-sort" data-key="naziv">Savez</th>';
html += '<th class="ri-sort" data-key="razina">Razina</th>';
html += '<th class="ri-sort" data-key="grad">Grad</th>';
html += '<th class="ri-sort" data-key="predsjednik">Predsjednik</th>';
html += '</tr></thead><tbody>';
items.forEach(s => {
html += '<tr><td><b>' + (s.naziv||'') + '</b></td><td>' + (s.razina||'-') + '</td><td class="muted">' + (s.grad||'-') + '</td><td>' + (s.predsjednik||'-') + '</td></tr>';
});
html += '</tbody></table></div></div></div>';
const div = document.createElement('div');
div.innerHTML = html;
document.body.appendChild(div.firstChild);
attachTableSort(document.querySelector('#riModal .ri-sortable'));
});
}
function closeRiModal() {
const m = document.getElementById('riModal');
if (m) m.remove();
}
// === Table sort ===
function attachTableSort(tableEl) {
if (!tableEl) return;
const ths = tableEl.querySelectorAll('th.ri-sort');
ths.forEach(th => {
th.style.cursor = 'pointer';
th.style.userSelect = 'none';
th.addEventListener('click', () => {
const key = th.dataset.key;
const tbody = tableEl.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
const isNum = th.classList.contains('num');
const cur = th.dataset.dir || '';
const dir = cur === 'asc' ? 'desc' : 'asc';
// clear other ths
tableEl.querySelectorAll('th.ri-sort').forEach(t2 => {
t2.dataset.dir = '';
const arr = t2.querySelector('.ri-sort-arrow');
if (arr) arr.remove();
});
th.dataset.dir = dir;
const arrow = document.createElement('span');
arrow.className = 'ri-sort-arrow';
arrow.textContent = dir === 'asc' ? ' ↑' : ' ↓';
arrow.style.color = 'var(--accent)';
arrow.style.fontSize = '10px';
th.appendChild(arrow);
// Sort rows by cell at index of th
const idx = Array.from(th.parentElement.children).indexOf(th);
rows.sort((a, b) => {
const av = a.children[idx]?.innerText.trim() || '';
const bv = b.children[idx]?.innerText.trim() || '';
let cmp;
if (isNum) {
cmp = (parseFloat(av.replace(/[^\d.\-]/g,'')) || 0) - (parseFloat(bv.replace(/[^\d.\-]/g,'')) || 0);
} else {
cmp = av.localeCompare(bv, 'hr', {numeric: true});
}
return dir === 'asc' ? cmp : -cmp;
});
rows.forEach(r => tbody.appendChild(r));
});
});
}
// Auto-attach sort on every page render
function autoAttachSort() {
document.querySelectorAll('table.ri-sortable').forEach(t => {
if (!t.dataset.sortAttached) {
attachTableSort(t);
t.dataset.sortAttached = '1';
}
});
}
// === Unified Lucide icons (SVG) ===
function iconEye() { return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>'; }
function iconEdit() { return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>'; }
function iconShare() { return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>'; }
function iconDownload() { return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>'; }
function iconX() { return '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>'; }
function iconUser() { return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>'; }
function iconFile() { return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>'; }
function iconExternal() { return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>'; }
function iconMic() { return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>'; }
// === GUI UPGRADE end ===
const routes = { graf:pageGraf, dms:pageDMS, dataQuality:pageDataQuality, search:pageSearch, dashboard:pageDashboard, analytics:pageAnalytics, alertovi:pageAlertovi, savezi:pageSavezi, klubovi:pageKlubovi, clanovi:pageClanovi, clanarine:pageClanarine, potpore:pagePotpore, proracun:pageProracun, lijecnicki:pageLijecnicki, zzjz:pageZzjz, manifestacije:pageManifestacije, statistika:pageStatistika , ask:pageAsk, invoices:pageInvoices, expenses:pageExpenses, forms:pageForms, users:pageUsers, pravnik:pagePravnik, natjecanja:pageNatjecanja, admin:pageAdmin , sportStats:pageSportStats, baza:pageBaza, dokumenti:pageDokumenti, kategorije:pageKategorije, funkcionari:pageFunkcionari, sportasi:pageSportasi, sportas:pageSportas, klubRoster:pageKlubRoster, sport:pageSport, audit:pageAudit, matrix:pageMatrix, coverage:pageCoverage, natjecanja:pageNatjecanja, natjecanjaTablica:pageNatjecanjaTablica, godisnjaci:pageGodisnjaci, dokumentDetail:pageDokumentDetail };
// ===== V6.2 VOICE INPUT (hr-HR) =====
window._v6Recognition = null;
window._v6CurrentInput = null;
function v6VoiceInit() {
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SR) return null;
const r = new SR();
r.lang = 'hr-HR';
r.continuous = false;
r.interimResults = true;
r.maxAlternatives = 1;
return r;
}
function v6VoiceStart(inputId, btnEl) {
const inp = document.getElementById(inputId);
if (!inp) return;
if (!window._v6Recognition) window._v6Recognition = v6VoiceInit();
const rec = window._v6Recognition;
if (!rec) {
alert('Voice input nije podržan u ovom pregledniku. Koristi Chrome ili Edge.');
return;
}
// If already recording, stop
if (btnEl && btnEl.classList.contains('recording')) {
try { rec.stop(); } catch(e){}
btnEl.classList.remove('recording');
btnEl.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>';
return;
}
if (btnEl) {
btnEl.classList.add('recording');
btnEl.innerHTML = '■';
}
let finalTranscript = '';
rec.onresult = function(ev) {
let interim = '';
for (let i = ev.resultIndex; i < ev.results.length; i++) {
if (ev.results[i].isFinal) finalTranscript += ev.results[i][0].transcript;
else interim += ev.results[i][0].transcript;
}
inp.value = finalTranscript + interim;
};
rec.onerror = function(ev) {
console.warn('voice err', ev.error);
if (btnEl) { btnEl.classList.remove('recording'); btnEl.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>'; }
};
rec.onend = function() {
if (btnEl) { btnEl.classList.remove('recording'); btnEl.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>'; }
// If we got final transcript and inp is part of search/ask form, auto-submit
if (finalTranscript) {
inp.value = finalTranscript.trim();
// Auto-submit based on input id
if (inputId === 'askQ' && typeof askGo === 'function') askGo();
else if (inputId === 'lawQ' && typeof lawGo === 'function') lawGo();
else if (inputId === 'aiSearchInline' && finalTranscript.trim()) {
state.searchQ = finalTranscript.trim();
render();
}
else if (inputId === 'searchInput' && finalTranscript.trim()) {
state.searchQ = finalTranscript.trim();
render();
}
}
};
try { rec.start(); }
catch(e) {
console.warn('voice start err', e);
if (btnEl) { btnEl.classList.remove('recording'); btnEl.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>'; }
}
}
// ===== V6.2 CHATBOT for AI Asistent =====
window.chatHistory = []; // array of {role:'user'|'bot', content, sources?, llm?, hits?}
function chatRender() {
const t = document.getElementById('chatThread');
if (!t) return;
t.innerHTML = chatHistory.map(function(m, i){
if (m.role === 'user') {
return '<div class="v6-chat-msg user">' + (m.content||'').replace(/</g,'&lt;') + '</div>';
} else if (m.role === 'typing') {
return '<div class="v6-chat-typing"><span></span><span></span><span></span></div>';
} else {
let metaHtml = '';
if (m.llm) metaHtml = '<div class="v6-msg-meta">🤖 ' + m.llm + (m.hits ? ' · '+m.hits+' izvora' : '') + '</div>';
// CLICKABLE RESULTS MODE (RAG)
if (m.isClickable && m.results) {
let bodyHtml = '<div style="margin-bottom:8px">Pronašao sam ' + m.totalCount + ' rezultata. Klikni za otvaranje:</div>';
bodyHtml += '<div style="display:flex;flex-direction:column;gap:6px">';
m.results.forEach(function(h, i) {
const title = h.title || (h.payload && h.payload.title) || '?';
const snippet = (h.snippet || '').slice(0, 180);
const score = (h.score*100).toFixed(0);
const tip = (h.payload && (h.payload.tip || h.payload.type)) || h.type || '';
const p = h.payload || {};
// Build click handler based on tip - use existing goto* functions
let onClick = '';
let icon = '👤';
if (tip === 'clan' && p.clan_id) {
onClick = `gotoSportas(${p.clan_id})`;
icon = '🏃';
} else if (tip === 'klub' && p.klub_id) {
onClick = `gotoKlubRoster(${p.klub_id})`;
icon = '🏛️';
} else if (tip === 'savez' && (p.savez_id || p.id)) {
onClick = `goto('savezi')`;
icon = '🏆';
} else if (tip === 'dokument' && (p.dokument_id || p.id)) {
onClick = `showDokViewerModal(${p.dokument_id || p.id})`;
icon = '📄';
} else if (tip === 'manifestacija' && p.manifestacija_id) {
onClick = `state.manifestacija_id=${p.manifestacija_id};goto('manifestacije')`;
icon = '🎯';
}
if (onClick) {
bodyHtml += '<div onclick="' + onClick.replace(/"/g,'&quot;') + '" style="cursor:pointer;padding:10px 12px;background:var(--bg-2);border-radius:6px;border-left:3px solid var(--accent);transition:background 0.2s" onmouseover="this.style.background=&apos;var(--bg-3)&apos;" onmouseout="this.style.background=&apos;var(--bg-2)&apos;">';
bodyHtml += '<div style="display:flex;align-items:center;gap:8px"><span>' + icon + '</span><b>' + title.replace(/</g,'&lt;') + '</b>';
bodyHtml += '<span style="margin-left:auto;color:var(--text3);font-size:11px">' + score + '%</span></div>';
if (snippet) bodyHtml += '<div class="muted" style="font-size:12px;margin-top:4px;line-height:1.4">' + snippet.replace(/</g,'&lt;') + '</div>';
bodyHtml += '</div>';
} else {
bodyHtml += '<div style="padding:10px 12px;background:var(--bg-2);border-radius:6px;opacity:0.6">';
bodyHtml += '<b>' + title.replace(/</g,'&lt;') + '</b> <span style="color:var(--text3);font-size:11px">' + score + '%</span>';
if (snippet) bodyHtml += '<div class="muted" style="font-size:12px;margin-top:4px">' + snippet.replace(/</g,'&lt;') + '</div>';
bodyHtml += '</div>';
}
});
bodyHtml += '</div>';
// AI enrich button if low scores or fewer results
if (m.query) {
// Use data attribute for safe escape, click handled below
const qEsc = (m.query || '').replace(/"/g, '&quot;').replace(/</g, '&lt;');
bodyHtml += `<div style="margin-top:12px;text-align:right"><button class="btn primary chat-enrich-btn" data-q="${qEsc}" style="font-size:12px">🔍 AI obogati pretragu (Internet)</button></div>`;
}
return '<div class="v6-chat-msg bot">' + metaHtml + bodyHtml + '</div>';
}
// Fallback: plain text + sources (lawyer mode etc.)
let body = (m.content||'').replace(/</g,'&lt;');
let srcHtml = '';
if (m.sources && m.sources.length) {
srcHtml = '<div style="margin-top:8px">' + m.sources.map(function(s){
const url = (s.payload && (s.payload.source_url || s.payload.url)) || s.url || '';
const title = s.title || (s.payload && s.payload.title) || '?';
if (url) return '<a class="v6-src-link" href="' + url.replace(/"/g,'&quot;') + '" target="_blank">📄 ' + (title.length>40?title.slice(0,40)+'…':title) + '</a>';
return '<span class="v6-src-link" style="background:#2a3a52">📌 '+title+'</span>';
}).join('') + '</div>';
}
// Show enrich button if no results
let enrichHtml = '';
if (m.enrichQuery) {
const qEsc2 = (m.enrichQuery || '').replace(/"/g, '&quot;').replace(/</g, '&lt;');
enrichHtml = `<div style="margin-top:12px;text-align:right"><button class="btn primary chat-enrich-btn" data-q="${qEsc2}" style="font-size:12px">🔍 AI obogati (Internet)</button></div>`;
}
return '<div class="v6-chat-msg bot">' + metaHtml + body + srcHtml + enrichHtml + '</div>';
}
}).join('');
t.scrollTop = t.scrollHeight;
}
async function chatSend(mode) {
// mode: 'rag' (askGo) or 'lawyer' (lawGo)
const inp = document.getElementById(mode === 'lawyer' ? 'lawQ' : 'askQ');
const q = inp.value.trim();
if (!q) return;
chatHistory.push({role:'user', content:q});
chatHistory.push({role:'typing'});
chatRender();
inp.value = '';
try {
let endpoint, payload;
if (mode === 'lawyer') {
endpoint = '/sport/api/v2/sport/lawyer';
// include conversation context (last 4 turns)
const ctx = chatHistory.slice(-8).filter(function(m){ return m.role !== 'typing'; })
.map(function(m){ return (m.role==='user'?'Q: ':'A: ') + (m.content||'').slice(0,300); }).join('\n');
payload = {query: q, context: ctx};
} else {
endpoint = '/sport/api/v2/sport/ask';
payload = {query: q, limit: 8};
}
const r = await fetch(endpoint, {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload)});
const d = await r.json();
// Remove typing indicator
chatHistory = chatHistory.filter(function(m){ return m.role !== 'typing'; });
if (mode === 'lawyer') {
chatHistory.push({
role: 'bot',
content: d.answer || d.detail || 'Nema odgovora.',
sources: d.sources,
llm: d.llm,
hits: d.hits_count
});
} else {
// RAG mode — clickable HTML rendering
if (!d.results || !d.results.length) {
chatHistory.push({role:'bot', content:'Nisam pronašao ništa relevantno za "'+q+'".', enrichQuery: q});
} else {
const top5 = d.results.slice(0, 5);
chatHistory.push({role:'bot', isClickable: true, query: q, results: top5, totalCount: d.results.length, llm:'rag', hits: d.results.length});
}
}
} catch(e) {
chatHistory = chatHistory.filter(function(m){ return m.role !== 'typing'; });
chatHistory.push({role:'bot', content: '⚠️ Greška: ' + e.message});
}
chatRender();
}
function chatReset() {
chatHistory = [];
chatRender();
const t = document.getElementById('chatThread');
if (t) t.innerHTML = '<div style="text-align:center;color:#788798;padding:40px;font-size:13px">💬 Postavi pitanje. Mogu odgovoriti glasovno (<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>) ili tekstom.</div>';
}
function render() {
const fn = routes[state.page] || pageDashboard;
fn().then(() => {
// Auto-attach sort to all .ri-sortable tables after render
setTimeout(() => { try { autoAttachSort(); } catch(e){} }, 50);
setTimeout(() => { try { autoAttachSort(); } catch(e){} }, 300);
}).catch(e => document.getElementById('content').innerHTML = `<div class="ban crit"><b>Greška:</b> ${e.message}</div>`);
}
// Global MutationObserver - re-attach sort on any DOM change in #content
window.addEventListener('DOMContentLoaded', () => {
const target = document.getElementById('content');
if (target && window.MutationObserver) {
const obs = new MutationObserver(() => { try { autoAttachSort(); } catch(e){} });
obs.observe(target, { childList:true, subtree:true });
}
});
document.getElementById('token-input').addEventListener('keypress', e => { if (e.key==='Enter') doLogin(); });
// ============ V2 ERP & PRAVO PAGES ============
async function v2Fetch(path, opts={}) {
const tok = localStorage.getItem('rinet_v2_token');
opts.headers = Object.assign({'Content-Type':'application/json'}, opts.headers||{}, tok?{Authorization:'Bearer '+tok}:{});
const r = await fetch('/sport/api/v2'+path, opts);
if (!r.ok) throw new Error(`${r.status}: ${await r.text()}`);
return r.json();
}
async function pageAsk() {
document.getElementById('content').innerHTML = `
<div class="page-h"><h2>AI Asistent</h2><p class="muted">Razgovaraj sa AI asistentom o klubovima, savezima, pravilnicima, financiranju. Tipkaj ili koristi glasovni unos <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg> (hr-HR).</p></div>
<div class="card">
<div id="chatThread" class="v6-chat-thread"></div>
<div class="v6-input-row">
<input id="askQ" class="inp" style="flex:1" placeholder="Postavi pitanje... (Enter za poslati)" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();chatSend('rag')}" />
<button class="v6-mic-btn" id="askMicBtn" onclick="v6VoiceStart('askQ', this)" title="Glasovni unos (hr-HR)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></button>
<button class="btn primary" onclick="chatSend('rag')">📤</button>
<button class="btn" onclick="chatReset()" title="Novi razgovor">Reset</button>
</div>
<div style="font-size:12px;color:var(--muted);margin-top:8px">Primjeri:
<a href="#" onclick="document.getElementById('askQ').value='Pravilnik o liječničkim pregledima sportaša';chatSend('rag');return false">Liječnički</a> ·
<a href="#" onclick="document.getElementById('askQ').value='Kako se financiraju javne potrebe u sportu PGŽ';chatSend('rag');return false">JP financiranje</a> ·
<a href="#" onclick="document.getElementById('askQ').value='NK Orijent Rijeka predsjednik';chatSend('rag');return false">NK Orijent</a> ·
<a href="#" onclick="document.getElementById('askQ').value='kotizacije za natjecanja u nogometu';chatSend('rag');return false">Kotizacije</a>
</div>
</div>`;
chatRender();
if (window.chatHistory.length === 0) {
const t = document.getElementById('chatThread');
if (t) t.innerHTML = '<div style="text-align:center;color:#788798;padding:40px;font-size:13px">💬 Postavi pitanje. Mogu odgovoriti glasovno (<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>) ili tekstom.</div>';
}
}
async function askGo() {
const q = document.getElementById('askQ').value.trim();
if (!q) return;
const out = document.getElementById('askOut');
out.innerHTML = '<div class="loader">Pretraga vektorske baze...</div>';
try {
const r = await fetch('/sport/api/v2/sport/ask', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({query:q, limit:10})});
const d = await r.json();
if (!d.results || !d.results.length) { out.innerHTML = '<div class="ban warn">Nema rezultata.</div>'; return; }
out.innerHTML = d.results.map(h => {
const url = (h.payload && (h.payload.source_url || h.payload.url)) || '';
const title = h.title || (h.payload && h.payload.title) || '(bez naslova)';
const tip = h.type || (h.payload && h.payload.tip) || '';
const klubId = h.payload && h.payload.klub_id;
const savezId = h.payload && h.payload.savez_id;
const docType = h.payload && h.payload.doc_type;
const sourceTag = h.payload && h.payload.source;
const publishDate = h.payload && h.payload.publish_date;
let click=''; let hint='';
if (url) { click='onclick="window.open(\''+url.replace(/\x27/g,'\\x27')+'\', \'_blank\')"'; hint='<span class="pill" style="background:#2a5e3a;color:#fff">otvori dokument</span>'; }
else if (klubId) { click='onclick="showKlub('+klubId+')"'; hint='<span class="pill ok">klub →</span>'; }
else if (savezId) { click='onclick="showSavez('+savezId+')"'; hint='<span class="pill ok">savez →</span>'; }
return '<div class="card" '+click+' style="margin-bottom:8px;cursor:'+(click?'pointer':'default')+';border-left:3px solid '+(h.score>0.7?'#27c79b':h.score>0.6?'#f0b429':'#7a7a7a')+'">'
+'<div style="display:flex;justify-content:space-between;align-items:start;gap:8px;flex-wrap:wrap">'
+'<div><b>'+title+'</b> <span class="pill">'+tip+'</span>'+(docType?' <span class="pill muted">'+docType+'</span>':'')+' '+hint+'</div>'
+'<div style="font-size:11px;color:var(--muted)">'+(publishDate?publishDate.slice(0,10)+' · ':'')+(sourceTag||'')+' · '+h.score.toFixed(3)+'</div>'
+'</div>'
+'<div style="margin-top:6px;font-size:13px;color:#bbb">'+(h.snippet||'').replace(/</g,'&lt;').slice(0,400)+((h.snippet||'').length>400?'…':'')+'</div>'
+(url?'<div style="margin-top:4px;font-size:11px;color:#5e72e4;word-break:break-all">'+(url.length>120?url.slice(0,120)+'…':url)+'</div>':'')
+'</div>';
}).join('');
} catch(e) { out.innerHTML = '<div class="ban crit">Greška: '+e.message+'</div>'; }
}
async function pagePravnik() {
document.getElementById('content').innerHTML = `
<div class="page-h"><h2>AI Pravnik</h2><p class="muted">Stručni pravni odgovori temeljeni na pravilnicima HOO, MINT-a, ZSPGŽ-a, klubova i statuta. RAG + DeepSeek/Groq.</p></div>
<div class="card" style="margin-bottom:14px">
<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap">
<input id="lawQ" class="inp" style="flex:1;min-width:300px" placeholder="npr. Kako kategorizirati odbojkaša? Koji su rokovi za prijavu?" onkeydown="if(event.key==='Enter')lawGo()" />
<button class="v6-mic-btn" onclick="v6VoiceStart('lawQ', this)" title="Glasovni unos (hr-HR)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></button>
<button class="btn primary" onclick="lawGo()">Pitaj Pravnika</button>
</div>
<div style="font-size:12px;color:var(--muted)">Primjeri:
<a href="#" onclick="document.getElementById('lawQ').value='Kako kategorizirati odbojkaša prema HOO pravilniku?';lawGo();return false">Kategorizacija odbojkaša</a> ·
<a href="#" onclick="document.getElementById('lawQ').value='Koji su uvjeti za sufinanciranje sportskog programa iz proračuna PGŽ?';lawGo();return false">Sufinanciranje programa</a> ·
<a href="#" onclick="document.getElementById('lawQ').value='Što je potrebno za prijavu sportaša u registar?';lawGo();return false">Registar sportaša</a> ·
<a href="#" onclick="document.getElementById('lawQ').value='Koje su obveze kluba za godišnje izvješće?';lawGo();return false">Godišnje izvješće</a> ·
<a href="#" onclick="document.getElementById('lawQ').value='Koliko često sportaš mora obaviti liječnički pregled?';lawGo();return false">Liječnički pregledi</a>
</div>
</div>
<div id="lawOut"></div>`;
document.getElementById('lawQ').addEventListener('keypress', e => { if (e.key==='Enter') lawGo(); });
}
async function lawGo() {
const q = document.getElementById('lawQ').value.trim();
if (!q) return;
const out = document.getElementById('lawOut');
out.innerHTML = '<div class="loader">AI Pravnik analizira...</div>';
try {
const r = await fetch('/sport/api/v2/sport/lawyer', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({query:q})});
const d = await r.json();
if (d.detail) { out.innerHTML = '<div class="ban crit">'+d.detail+'</div>'; return; }
let html = `<div class="card" style="border-left:3px solid #5e72e4;margin-bottom:14px">
<div style="font-size:11px;color:var(--muted);margin-bottom:8px">Pitanje: ${q}</div>
<div style="font-size:12px;color:var(--muted);margin-bottom:8px">LLM: <b>${d.llm}</b> · ${d.hits_count||0} pravilnika · ${d.sources?.length||0} citata</div>
<div style="white-space:pre-wrap;line-height:1.6;font-size:14px;color:var(--text)">${(d.answer||'').replace(/</g,'&lt;')}</div>
</div>`;
if (d.sources && d.sources.length) {
html += '<h3 style="margin:14px 0 8px">Reference</h3>';
d.sources.forEach((s,i) => {
const url = s.url || s.source_url || '';
const click = url ? 'onclick="window.open(\''+url.replace(/\x27/g,"\\x27")+'\',\'_blank\')" style="cursor:pointer"' : '';
html += `<div class="card" ${click} style="margin-bottom:6px;font-size:13px;transition:background 0.15s" onmouseover="this.style.background='#1a2330'" onmouseout="this.style.background=''">
<b>[${s.id}]</b> ${s.title} <span class="pill">${s.doc_type||'doc'}</span>
${url ? '<span class="pill" style="background:#2a5e3a;color:#fff">otvori dokument</span>' : ''}
<span style="float:right;color:var(--muted);font-size:11px">score ${(s.score||0).toFixed(3)}</span>
${url ? '<br><span style="font-size:11px;color:#5e72e4;word-break:break-all">'+(url.length>120?url.slice(0,120)+'…':url)+'</span>' : ''}
</div>`;
});
}
out.innerHTML = html;
} catch(e) { out.innerHTML = '<div class="ban crit">Greška: '+e.message+'</div>'; }
}
async function pageNatjecanja() {
document.getElementById('content').innerHTML = `
<div class="page-h"><h2>Natjecanja</h2><p class="muted">Sve lige i natjecanja klubova PGŽ — nogomet, košarka, rukomet, odbojka, vaterpolo i ostali sportovi.</p></div>
<div class="card" style="margin-bottom:12px">
<div style="display:flex;gap:8px;flex-wrap:wrap">
<select id="natSport" class="inp" onchange="loadNatj()">
<option value="">Svi sportovi</option>
</select>
<select id="natRazina" class="inp" onchange="loadNatj()">
<option value="">Sve razine</option>
<option value="zupanijski">Županijska (PGŽ)</option>
<option value="nacionalni">Nacionalna</option>
<option value="ostalo">Ostalo</option>
</select>
<input id="natQ" class="inp" placeholder="Pretraži..." onkeyup="loadNatj()" />
</div>
</div>
<div id="natList" class="loader">Učitavanje...</div>`;
await loadNatjFilters();
await loadNatj();
}
async function loadNatjFilters() {
try {
const r = await fetch('/sport/api/natjecanja/filters');
if (r.ok) {
const d = await r.json();
const sel = document.getElementById('natSport');
if (sel && d.sports) {
d.sports.forEach(s => sel.innerHTML += '<option value="'+s+'">'+s+'</option>');
}
}
} catch (e) {}
}
async function loadNatj() {
const sport = document.getElementById('natSport').value;
const razina = document.getElementById('natRazina').value;
const q = document.getElementById('natQ').value.trim();
const out = document.getElementById('natList');
out.innerHTML = '<div class="loader">Učitavanje...</div>';
try {
let url = '/sport/api/natjecanja?limit=200';
if (sport) url += '&sport=' + encodeURIComponent(sport);
if (razina) url += '&razina=' + encodeURIComponent(razina);
if (q) url += '&q=' + encodeURIComponent(q);
const r = await fetch(url);
const d = await r.json();
if (!d.results || !d.results.length) { out.innerHTML = '<div class="empty">Nema natjecanja</div>'; return; }
let html = '<div style="margin-bottom:8px;color:var(--muted);font-size:12px">'+d.count+' natjecanja</div>';
html += '<table class="t"><tr><th>Sport</th><th>Razina</th><th>Naziv</th><th>Sezona</th><th>Kategorija</th><th>URL</th></tr>';
d.results.forEach(n => {
html += '<tr>'
+ '<td><span class="pill">'+(n.sport||'-')+'</span></td>'
+ '<td><span class="pill '+(n.razina==='zupanijski'?'ok':'muted')+'">'+(n.razina||'-')+'</span></td>'
+ '<td>'+(n.naziv||'-')+'</td>'
+ '<td>'+(n.sezona||'-')+'</td>'
+ '<td>'+(n.kategorija||'-')+'</td>'
+ '<td>'+(n.external_url ? '<a href="'+n.external_url+'" target="_blank" style="color:#5e72e4">otvori →</a>' : '-')+'</td>'
+ '</tr>';
});
html += '</table>';
out.innerHTML = html;
} catch(e) { out.innerHTML = '<div class="ban crit">'+e.message+'</div>'; }
}
async function pageAdmin() {
document.getElementById('content').innerHTML = `
<div class="page-h"><h2>Admin · System</h2><p class="muted">Upravljanje korisnicima portala. Multi-tenant po klubovima i savezima. Blockchain audit.</p></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px">
<div class="card" id="adminStats">
<h3>Sažetak</h3>
<div id="adminStatsContent" class="loader">Učitavanje...</div>
</div>
<div class="card">
<h3>Novi korisnik</h3>
<div style="display:flex;flex-direction:column;gap:6px">
<input id="newEmail" class="inp" placeholder="Email" />
<div style="display:flex;gap:6px">
<input id="newIme" class="inp" placeholder="Ime" style="flex:1" />
<input id="newPrezime" class="inp" placeholder="Prezime" style="flex:1" />
</div>
<select id="newType" class="inp">
<option value="pgz_admin">pgz_admin (PGŽ Odjel sporta — sve)</option>
<option value="pgz_user">pgz_user (PGŽ Odjel sporta — pregled)</option>
<option value="pgz_finance">pgz_finance (PGŽ Finance)</option>
<option value="pgz_zzjz">pgz_zzjz (ZZJZ medical)</option>
<option value="savez_admin">savez_admin (Tajnik saveza)</option>
<option value="savez_user">savez_user (Pomoćnik saveza)</option>
<option value="klub_admin">klub_admin (Tajnik kluba)</option>
<option value="klub_user">klub_user (Pomoćnik kluba)</option>
<option value="klub_clan">klub_clan (Sportaš)</option>
</select>
<input id="newKlubId" class="inp" placeholder="klub_id (opcionalno)" />
<input id="newSavezId" class="inp" placeholder="savez_id (opcionalno)" />
<input id="newPwd" class="inp" type="password" placeholder="Privremeni password" />
<button class="btn primary" onclick="createUser()"> Stvori korisnika</button>
<div id="newUserStatus" style="font-size:12px;color:var(--muted)"></div>
</div>
</div>
</div>
<div class="card" style="margin-bottom:14px">
<h3>Svi korisnici</h3>
<div style="display:flex;gap:8px;margin-bottom:8px">
<input id="adminQ" class="inp" placeholder="Pretraži po emailu/imenu" onkeyup="loadUsers()" style="flex:1" />
<select id="adminFilterType" class="inp" onchange="loadUsers()">
<option value="">Svi tipovi</option>
<option value="pgz_admin">pgz_admin</option>
<option value="pgz_user">pgz_user</option>
<option value="pgz_finance">pgz_finance</option>
<option value="pgz_zzjz">pgz_zzjz</option>
<option value="savez_admin">savez_admin</option>
<option value="klub_admin">klub_admin</option>
<option value="klub_user">klub_user</option>
<option value="klub_clan">klub_clan</option>
</select>
</div>
<div id="usersList" class="loader">Učitavanje...</div>
</div>
<div class="card" style="margin-bottom:14px">
<h3>Multi-tenant veze</h3>
<p class="muted" style="font-size:12px">Korisnik može imati prava na više klubova/saveza s različitim ulogama (tajnik, predsjednik, sportaš, trener...)</p>
<div style="display:flex;gap:8px;margin-bottom:8px;flex-wrap:wrap">
<input id="linkUserId" class="inp" placeholder="user_id" type="number" />
<input id="linkKlubId" class="inp" placeholder="klub_id" type="number" />
<input id="linkSavezId" class="inp" placeholder="ili savez_id" type="number" />
<select id="linkRole" class="inp">
<option value="tajnik">Tajnik</option>
<option value="predsjednik">Predsjednik</option>
<option value="clan_uprave">Član uprave</option>
<option value="trener">Trener</option>
<option value="sportas">Sportaš</option>
<option value="volonter">Volonter</option>
<option value="clan">Član</option>
</select>
<button class="btn primary" onclick="createKlubLink()">🔗 Dodaj vezu</button>
</div>
<div id="klubLinksList" class="loader">Učitavanje...</div>
</div>
<div class="card" style="margin-bottom:14px">
<h3>Audit Log <span class="pill" id="chainStatus">verifying...</span></h3>
<p class="muted" style="font-size:12px">Sve akcije u sustavu zapisuju se u hash-chained ledger. Svaka izmjena uvjetuje potpis prethodnog reda. Ne može se neopaženo izmijeniti.</p>
<div style="display:flex;gap:8px;margin-bottom:8px">
<input id="auditQ" class="inp" placeholder="Filter po akciji" onkeyup="loadAuditChain()" style="flex:1" />
<button class="btn" onclick="verifyChain()">🔍 Verify Chain</button>
<button class="btn" onclick="loadAuditChain()">↻ Refresh</button>
</div>
<div id="auditList" class="loader">Učitavanje...</div>
</div>
<div class="card">
<h3>Permission matrica</h3>
<div id="permMatrix" class="loader">Učitavanje...</div>
</div>`;
await loadAdminStats();
await loadUsers();
await loadKlubLinks();
await loadAuditChain();
await verifyChain();
await loadPermMatrix();
}
async function loadKlubLinks() {
try {
const r = await fetch('/sport/api/admin/klub-links');
const d = await r.json();
let html = '<table class="t" style="font-size:12px"><tr><th>ID</th><th>Korisnik</th><th>Klub/Savez</th><th>Uloga</th><th>Aktivan</th><th>Od</th><th>Akcije</th></tr>';
(d.results||[]).forEach(l => {
const subj = l.klub_naziv || l.savez_naziv || '?';
const subjType = l.klub_id ? '<span class="pill ok">klub</span>' : '<span class="pill info">savez</span>';
html += '<tr>'
+ '<td>'+l.id+'</td>'
+ '<td>'+(l.email||'?')+'<br><span class="muted" style="font-size:10px">'+(l.ime||'')+' '+(l.prezime||'')+'</span></td>'
+ '<td>'+subjType+' '+subj+' (#'+(l.klub_id||l.savez_id)+')</td>'
+ '<td><span class="pill">'+l.role+'</span>'+(l.primary_link?' <span class="pill ok">primary</span>':'')+'</td>'
+ '<td>'+(l.aktivan ? '✅' : '❌')+'</td>'
+ '<td style="font-size:10px">'+(l.granted_at||'').slice(0,10)+'</td>'
+ '<td><a href="#" onclick="deleteKlubLink('+l.id+');return false">✕</a></td>'
+ '</tr>';
});
if (!(d.results||[]).length) html += '<tr><td colspan="7" style="text-align:center;color:var(--muted)">Nema veza</td></tr>';
html += '</table>';
document.getElementById('klubLinksList').innerHTML = html;
} catch (e) { document.getElementById('klubLinksList').innerHTML = '<div class="ban crit">'+e.message+'</div>'; }
}
async function createKlubLink() {
const body = {
user_id: parseInt(document.getElementById('linkUserId').value),
klub_id: parseInt(document.getElementById('linkKlubId').value)||null,
savez_id: parseInt(document.getElementById('linkSavezId').value)||null,
role: document.getElementById('linkRole').value
};
if (!body.user_id || (!body.klub_id && !body.savez_id)) { alert('user_id + (klub_id ili savez_id) obavezno'); return; }
try {
const r = await fetch('/sport/api/admin/klub-links', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)});
const d = await r.json();
if (d.id) {
document.getElementById('linkUserId').value='';
document.getElementById('linkKlubId').value='';
document.getElementById('linkSavezId').value='';
loadKlubLinks();
} else alert(d.detail||'greška');
} catch (e) { alert(e.message); }
}
async function deleteKlubLink(id) {
if (!confirm('Obrisati vezu #'+id+'?')) return;
try { await fetch('/sport/api/admin/klub-links/'+id, {method:'DELETE'}); loadKlubLinks(); } catch (e) { alert(e.message); }
}
async function loadAuditChain() {
const q = (document.getElementById('auditQ')||{value:''}).value;
let url = '/sport/api/admin/audit-chain?limit=50';
if (q) url += '&action='+encodeURIComponent(q);
try {
const r = await fetch(url);
const d = await r.json();
let html = '<table class="t" style="font-size:11px"><tr><th>#</th><th>Vrijeme</th><th>Akcija</th><th>Target</th><th>User</th><th>Hash</th><th>Prev</th></tr>';
d.forEach(a => {
html += '<tr>'
+ '<td>'+a.chain_idx+'</td>'
+ '<td style="white-space:nowrap">'+(a.created_at||'').slice(0,16).replace('T',' ')+'</td>'
+ '<td><b>'+a.action+'</b></td>'
+ '<td>'+(a.target_type||'-')+(a.target_id?' #'+a.target_id:'')+(a.target_text?'<br><span class="muted" style="font-size:10px">'+a.target_text.slice(0,80)+'</span>':'')+'</td>'
+ '<td>'+(a.user_email||'system')+'</td>'
+ '<td><code style="font-size:10px;color:#27c79b">'+a.row_hash+'</code></td>'
+ '<td><code style="font-size:10px;color:var(--muted)">'+a.prev_hash+'</code></td>'
+ '</tr>';
});
html += '</table>';
if (!d.length) html = '<div class="empty">Nema audit zapisa</div>';
document.getElementById('auditList').innerHTML = html;
} catch(e) { document.getElementById('auditList').innerHTML = '<div class="ban crit">'+e.message+'</div>'; }
}
async function verifyChain() {
document.getElementById('chainStatus').innerHTML = 'verifying...';
try {
const r = await fetch('/sport/api/admin/audit-chain/verify');
const d = await r.json();
if (d.valid) {
document.getElementById('chainStatus').innerHTML = '<span style="color:#27c79b">OK</span> · '+d.total_rows+' rows · last hash: '+(d.last_hash||'').slice(0,16)+'…';
document.getElementById('chainStatus').className = 'pill ok';
} else {
document.getElementById('chainStatus').innerHTML = '<span style="color:#e74c3c">BROKEN at chain_idx '+d.broken_at.chain_idx+'</span>';
document.getElementById('chainStatus').className = 'pill crit';
}
} catch(e) { document.getElementById('chainStatus').innerHTML = 'err: '+e.message; }
}
async function loadAdminStats() {
try {
const r = await fetch('/sport/api/admin/stats');
const d = await r.json();
document.getElementById('adminStatsContent').innerHTML = `
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:8px">
<div><b>${d.users_total||0}</b><br><span class="muted" style="font-size:11px">korisnika</span></div>
<div><b>${d.users_active||0}</b><br><span class="muted" style="font-size:11px">aktivnih</span></div>
<div><b>${d.permissions_total||0}</b><br><span class="muted" style="font-size:11px">dozvola</span></div>
<div><b>${d.audit_today||0}</b><br><span class="muted" style="font-size:11px">akcija danas</span></div>
</div>
<h4 style="margin-top:14px;margin-bottom:6px">Po tipu korisnika</h4>
<table class="t" style="font-size:12px">
${(d.by_type||[]).map(r => '<tr><td>'+r.user_type+'</td><td><b>'+r.cnt+'</b></td></tr>').join('')}
</table>`;
} catch(e) { document.getElementById('adminStatsContent').innerHTML = '<div class="ban crit">'+e.message+'</div>'; }
}
async function loadUsers() {
const q = document.getElementById('adminQ').value.trim();
const tp = document.getElementById('adminFilterType').value;
let url = '/sport/api/admin/users?limit=100';
if (q) url += '&q='+encodeURIComponent(q);
if (tp) url += '&user_type='+tp;
try {
const r = await fetch(url);
const d = await r.json();
let html = '<table class="t"><tr><th>ID</th><th>Email</th><th>Ime</th><th>Tip</th><th>Klub</th><th>Savez</th><th>Aktivan</th><th>Akcije</th></tr>';
(d.results || []).forEach(u => {
html += '<tr>'
+ '<td>'+u.id+'</td>'
+ '<td><b>'+u.email+'</b></td>'
+ '<td>'+(u.ime||'')+' '+(u.prezime||'')+'</td>'
+ '<td><span class="pill">'+u.user_type+'</span></td>'
+ '<td>'+(u.klub_id||'-')+'</td>'
+ '<td>'+(u.savez_id||'-')+'</td>'
+ '<td>'+(u.aktivan ? '✅' : '❌')+'</td>'
+ '<td><a href="#" onclick="editUser('+u.id+');return false">edit</a> · <a href="#" onclick="toggleUser('+u.id+');return false">toggle</a></td>'
+ '</tr>';
});
html += '</table>';
document.getElementById('usersList').innerHTML = html;
} catch(e) { document.getElementById('usersList').innerHTML = '<div class="ban crit">'+e.message+'</div>'; }
}
async function loadPermMatrix() {
try {
const r = await fetch('/sport/api/admin/permissions-matrix');
const d = await r.json();
let html = '<div style="overflow-x:auto"><table class="t" style="font-size:11px"><tr><th>Kategorija</th><th>Permission</th>';
const types = d.user_types || [];
types.forEach(t => html += '<th>'+t+'</th>');
html += '</tr>';
(d.matrix || []).forEach(row => {
html += '<tr><td>'+row.kategorija+'</td><td><b>'+row.code+'</b><br><span class="muted">'+row.naziv+'</span></td>';
types.forEach(t => {
const has = (row.granted_to || []).includes(t);
html += '<td style="text-align:center">'+(has ? '✅' : '')+'</td>';
});
html += '</tr>';
});
html += '</table></div>';
document.getElementById('permMatrix').innerHTML = html;
} catch(e) { document.getElementById('permMatrix').innerHTML = '<div class="ban crit">'+e.message+'</div>'; }
}
async function createUser() {
const body = {
email: document.getElementById('newEmail').value.trim(),
ime: document.getElementById('newIme').value.trim(),
prezime: document.getElementById('newPrezime').value.trim(),
user_type: document.getElementById('newType').value,
klub_id: parseInt(document.getElementById('newKlubId').value) || null,
savez_id: parseInt(document.getElementById('newSavezId').value) || null,
password: document.getElementById('newPwd').value,
};
if (!body.email || !body.password) { alert('Email + password su obavezni'); return; }
try {
const r = await fetch('/sport/api/admin/users', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)});
const d = await r.json();
if (d.id) {
document.getElementById('newUserStatus').innerHTML = '✅ Stvoren korisnik #'+d.id;
document.getElementById('newEmail').value = '';
document.getElementById('newIme').value = '';
document.getElementById('newPrezime').value = '';
document.getElementById('newPwd').value = '';
loadUsers(); loadAdminStats();
} else {
document.getElementById('newUserStatus').innerHTML = '❌ ' + (d.detail || 'greška');
}
} catch(e) { document.getElementById('newUserStatus').innerHTML = '❌ '+e.message; }
}
async function toggleUser(id) {
if (!confirm('Toggle aktivan status korisnika #'+id+'?')) return;
try {
await fetch('/sport/api/admin/users/'+id+'/toggle', {method:'POST'});
loadUsers();
} catch(e) { alert(e.message); }
}
function editUser(id) { alert('Edit user '+id+' — TODO: full edit modal'); }
async function pageInvoices() {
document.getElementById('content').innerHTML = `
<div class="page-h"><h2>Računi · ERP</h2><p class="muted">Upload računa s OCR-om · IFRS knjigovodstvo · obveze/potraživanja</p></div>
<div class="card" style="margin-bottom:12px">
<h3>Upload računa (PDF/JPG/PNG)</h3>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<input type="file" id="invFile" accept=".pdf,.jpg,.jpeg,.png" />
<select id="invKind" class="inp"><option value="ulazni">Ulazni</option><option value="izlazni">Izlazni</option></select>
<input id="invKlub" class="inp" placeholder="klub_id (npr. 524)" />
<button class="btn primary" onclick="uploadInvoice()">Upload + OCR</button>
</div>
<div id="invUpStatus" style="margin-top:8px;font-size:13px"></div>
</div>
<div class="card">
<h3>OCR red + Uneseni računi</h3>
<div id="invList" class="loader">Učitavanje…</div>
</div>`;
await loadInvoices();
}
async function loadInvoices() {
try {
const ups = await v2Fetch('/invoice-uploads?limit=20');
const invs = await v2Fetch('/invoices?limit=20');
let html = '';
if (ups.length) {
html += '<h4 style="margin-top:0">📥 OCR red ('+ups.length+')</h4><table class="t"><tr><th>Datum</th><th>Status</th><th>Vendor</th><th>Iznos</th><th>Br.</th><th>Akcija</th></tr>';
ups.forEach(u => html += `<tr>
<td>${(u.uploaded_at||'').slice(0,16).replace('T',' ')}</td>
<td><span class="pill ${u.ocr_status==='done'?'ok':u.ocr_status==='failed'?'crit':'warn'}">${u.ocr_status}</span></td>
<td>${u.ai_vendor_name||''}</td>
<td>${u.ai_amount_gross?u.ai_amount_gross+' EUR':''}</td>
<td>${u.ai_invoice_no||''}</td>
<td><a href="#" onclick="detailUpload(${u.id});return false">detalji</a></td></tr>`);
html += '</table>';
}
if (invs.length) {
html += '<h4>📑 Uneseni računi ('+invs.length+')</h4><table class="t"><tr><th>Br.</th><th>Datum</th><th>Vendor</th><th>Iznos</th><th>Status</th></tr>';
invs.forEach(i => html += `<tr><td>${i.invoice_no||''}</td><td>${(i.invoice_date||'').slice(0,10)}</td><td>${i.vendor_name||''}</td><td>${i.amount_gross} ${i.currency}</td><td><span class="pill ${i.payment_status==='paid'?'ok':'warn'}">${i.payment_status}</span></td></tr>`);
html += '</table>';
}
if (!html) html = '<div class="muted">Nema računa. Upload prvi.</div>';
document.getElementById('invList').innerHTML = html;
} catch(e) { document.getElementById('invList').innerHTML = '<div class="ban crit">Login potreban: '+e.message+'</div>'; }
}
async function uploadInvoice() {
const f = document.getElementById('invFile').files[0];
const kind = document.getElementById('invKind').value;
let klub_id = (typeof getPickerKlubId === 'function') ? getPickerKlubId('invKlub') : 0;
if (!klub_id) {
// Fallback - try parse from raw value
const v = (document.getElementById('invKlub') || {}).value || '';
const m = v.match(/#(\d+)/);
klub_id = m ? parseInt(m[1]) : parseInt(v) || 0;
}
if (!f) { alert('Odaberi datoteku'); return; }
if (!klub_id) { alert('Klub nije izabran. Upiši ime kluba (npr. Zamet) i klikni iz padajuce liste'); return; }
const tok = localStorage.getItem('rinet_v2_token');
if (!tok) { alert('Login potreban'); return; }
const fd = new FormData();
fd.append('file', f); fd.append('klub_id', klub_id); fd.append('invoice_kind', kind);
document.getElementById('invUpStatus').innerHTML = '⏳ Upload + OCR queue…';
try {
const r = await fetch('/sport/api/v2/invoice-uploads/file', {method:'POST', headers:{Authorization:'Bearer '+tok}, body: fd});
const d = await r.json();
if (!r.ok) throw new Error(d.detail||r.status);
document.getElementById('invUpStatus').innerHTML = `✅ ID ${d.upload_id} u OCR redu (${d.ocr_status})`;
setTimeout(loadInvoices, 1500);
} catch(e) { document.getElementById('invUpStatus').innerHTML = '❌ '+e.message; }
}
async function detailUpload(id) {
let bd = document.querySelector('.ri-modal');
if (bd) bd.remove();
bd = document.createElement('div');
bd.className = 'ri-modal';
bd.id = 'riModal';
bd.onclick = (e) => { if (e.target === bd) closeRiModal(); };
bd.innerHTML = `<div class="ri-modal-box" style="max-width:800px">
<div class="ri-modal-h"><b>Detalji uploada #${id}</b>
<button class="ri-icon-btn" onclick="closeRiModal()">${iconX()}</button></div>
<div class="ri-modal-body" id="invDetailBody" style="padding:20px"><div class="muted">Učitavam...</div></div>
</div>`;
document.body.appendChild(bd);
try {
const r = await fetch('/sport/api/v2/invoice-uploads/' + id, {
headers: {'Authorization':'Bearer '+(localStorage.getItem('rinet_v2_token')||'')}
});
if (!r.ok) throw new Error('API ' + r.status);
const d = await r.json();
const body = document.getElementById('invDetailBody');
let h = '<table class="ri-tbl"><tbody>';
for (const [k, v] of Object.entries(d)) {
if (v === null || v === undefined) continue;
let val = v;
if (k === 'extracted_json' && typeof v === 'object') {
val = '<pre style="white-space:pre-wrap;font-size:11px;background:var(--bg-2);padding:8px;border-radius:4px;max-height:300px;overflow:auto">' + JSON.stringify(v, null, 2) + '</pre>';
} else if (typeof v === 'object') {
val = '<pre style="font-size:11px">' + JSON.stringify(v) + '</pre>';
} else {
val = String(v).replace(/</g,'&lt;');
}
h += `<tr><td style="font-weight:600;width:30%;color:var(--text3)">${k}</td><td>${val}</td></tr>`;
}
h += '</tbody></table>';
body.innerHTML = h;
} catch(e) {
document.getElementById('invDetailBody').innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>';
}
}
async function pageExpenses() {
document.getElementById('content').innerHTML = `
<div class="page-h"><h2>Putni nalozi & obračuni</h2><p class="muted">0,50 EUR/km vlastiti auto (Pravilnik NN 143/23) · 30 EUR/dnevnica HR</p></div>
<div class="card" style="margin-bottom:12px">
<h3>Novi putni nalog</h3>
<div class="grid" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px">
<input id="exKlub" class="inp" placeholder="klub_id" />
<select id="exType" class="inp">
<option value="putni_nalog">Putni nalog</option>
<option value="vlastiti_auto">Vlastiti auto</option>
<option value="dnevnice">Dnevnice</option>
</select>
<input id="exDest" class="inp" placeholder="Destinacija (npr. Zagreb)" />
<input id="exFrom" class="inp" type="date" />
<input id="exTo" class="inp" type="date" />
<input id="exKm" class="inp" type="number" placeholder="km (vlastiti auto)" />
<input id="exDni" class="inp" type="number" placeholder="dani dnevnica" />
<input id="exTransp" class="inp" type="number" placeholder="trošak prijevoza EUR" />
<input id="exHotel" class="inp" type="number" placeholder="trošak smještaja EUR" />
</div>
<button class="btn primary" style="margin-top:8px" onclick="saveExpense()">💾 Spremi</button>
<div id="exStatus" style="margin-top:8px"></div>
</div>
<div class="card">
<h3>Postojeći obračuni</h3>
<div id="exList" class="loader">Učitavanje…</div>
</div>`;
await loadExpenses();
}
async function loadExpenses() {
try {
const ex = await v2Fetch('/expense-reports?limit=20');
if (!ex.length) { document.getElementById('exList').innerHTML = '<div class="muted">Nema obračuna.</div>'; return; }
let html = '<table class="t"><tr><th>Datum</th><th>Tip</th><th>Destinacija</th><th>km</th><th>Ukupno</th><th>Status</th></tr>';
ex.forEach(e => html += `<tr><td>${(e.date_from||'').slice(0,10)}</td><td>${e.report_type}</td><td>${e.destination||''}</td><td>${e.km_driven||0}</td><td><b>${e.cost_total} EUR</b></td><td><span class="pill">${e.status}</span></td></tr>`);
html += '</table>';
document.getElementById('exList').innerHTML = html;
} catch(e) { document.getElementById('exList').innerHTML = '<div class="ban crit">Login potreban: '+e.message+'</div>'; }
}
async function saveExpense() {
const body = {
klub_id: parseInt(document.getElementById('exKlub').value||'0'),
report_type: document.getElementById('exType').value,
destination: document.getElementById('exDest').value,
date_from: document.getElementById('exFrom').value,
date_to: document.getElementById('exTo').value,
km_driven: parseFloat(document.getElementById('exKm').value||'0'),
dnevnice_count: parseInt(document.getElementById('exDni').value||'0'),
cost_transport: parseFloat(document.getElementById('exTransp').value||'0'),
cost_lodging: parseFloat(document.getElementById('exHotel').value||'0')
};
if (!body.klub_id || !body.date_from) { alert('Unesi klub i datum.'); return; }
try {
const d = await v2Fetch('/expense-reports', {method:'POST', body: JSON.stringify(body)});
document.getElementById('exStatus').innerHTML = `✅ Obračun #${d.report_id} | total ${d.cost_total} EUR`;
setTimeout(loadExpenses, 800);
} catch(e) { document.getElementById('exStatus').innerHTML = '❌ '+e.message; }
}
async function pageForms() {
document.getElementById('content').innerHTML = `
<div class="page-h"><h2>Obrasci</h2><p class="muted">8 templatea · prijave · sufinanciranje · liječnički · putni · godišnji</p></div>
<div id="formsList" class="loader">Učitavanje…</div>
<div id="formRender"></div>`;
try {
const tpls = await v2Fetch('/forms/templates');
document.getElementById('formsList').innerHTML = '<div class="grid" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:10px">' +
tpls.map(t => `<div class="card" style="cursor:pointer" onclick="openForm('${t.code}')">
<h4 style="margin:0 0 4px 0">${t.naziv}</h4>
<div class="muted" style="font-size:12px">${t.kategorija} · za: ${t.required_role||'svi'}</div>
<div style="margin-top:6px;font-size:12px">${t.field_count||(t.schema_json&&t.schema_json.fields?t.schema_json.fields.length:'?')} polja</div>
</div>`).join('') + '</div>';
} catch(e) { document.getElementById('formsList').innerHTML = '<div class="ban crit">'+e.message+'</div>'; }
}
async function openForm(code) {
try {
const tpl = await v2Fetch('/forms/templates/'+code);
let fields = (tpl.schema_json && tpl.schema_json.fields) || [];
// V6: Smart layout — group fields into 2-3 columns based on type/length
function colSpan(f) {
if (f.type === 'textarea' || f.name === 'napomena' || f.name === 'opis') return 2;
if (f.type === 'file') return 2;
if (f.name && (f.name.includes('adresa') || f.name.includes('napomena'))) return 2;
return 1;
}
// V6: Detect special form types
const isPutni = code === 'obracun_putnih_troskova' || code === 'putni_nalog';
const isSportas = code === 'prijava_sportasa';
let html = '<div class="v6-form" id="v6Form">';
html += ' <div class="v6-fh"><h3>📋 ' + tpl.naziv + '</h3>';
html += ' <div class="v6-actions">';
html += ' <button class="v6-btn" onclick="document.getElementById(\'formRender\').innerHTML=\'\'">Zatvori</button>';
html += ' </div></div>';
if (isPutni) {
// V6 PUTNI NALOG: Special AI-powered layout
html += ' <div class="v6-fs">';
html += ' <div class="v6-fs-t">Putovanje</div>';
html += ' <div class="v6-g3">';
html += ' <div class="v6-fld v6-ac">';
html += ' <label class="v6-lbl req">Polazište</label>';
html += ' <input id="ff_polaziste" class="v6-inp" type="text" placeholder="npr. Rijeka" oninput="v6GradAuto(this,\'polaziste\')" onchange="v6CalcKM()" />';
html += ' <div id="ac_polaziste" class="v6-ac-s"></div>';
html += ' </div>';
html += ' <div class="v6-fld v6-ac">';
html += ' <label class="v6-lbl req">Odredište</label>';
html += ' <input id="ff_odrediste" class="v6-inp" type="text" placeholder="npr. Zagreb" oninput="v6GradAuto(this,\'odrediste\')" onchange="v6CalcKM()" />';
html += ' <div id="ac_odrediste" class="v6-ac-s"></div>';
html += ' </div>';
html += ' <div class="v6-fld">';
html += ' <label class="v6-lbl">🤖 AI udaljenost (jedan pravac)</label>';
html += ' <input id="ff_ai_km" class="v6-inp v6-num v6-calc" type="number" step="0.1" readonly />';
html += ' </div>';
html += ' </div>';
html += ' <div id="aiHint" style="font-size:11px;color:#5e72e4;margin-top:6px"></div>';
html += ' </div>';
html += ' <div class="v6-fs">';
html += ' <div class="v6-fs-t">Datumi i kilometraža</div>';
html += ' <div class="v6-g4">';
html += ' <div class="v6-fld"><label class="v6-lbl req">Datum/sat polaska</label><input id="ff_datum_polaska" class="v6-inp" type="datetime-local" /></div>';
html += ' <div class="v6-fld"><label class="v6-lbl req">Datum/sat povratka</label><input id="ff_datum_povratka" class="v6-inp" type="datetime-local" /></div>';
html += ' <div class="v6-fld"><label class="v6-lbl">KM stanje na polasku</label><input id="ff_km_pre" class="v6-inp v6-num" type="number" oninput="v6CalcKM()" /></div>';
html += ' <div class="v6-fld"><label class="v6-lbl">KM stanje na povratku</label><input id="ff_km_post" class="v6-inp v6-num" type="number" oninput="v6CalcKM()" /></div>';
html += ' </div>';
html += ' <div class="v6-g4" style="margin-top:8px">';
html += ' <div class="v6-fld"><label class="v6-lbl req">Ukupno prijeđeno KM <span class="v6-pill">auto: 2× pravac ili stanje povratka stanje polaska</span></label><input id="ff_km_total" class="v6-inp v6-num v6-calc" type="number" step="0.1" oninput="v6CalcCost()" /></div>';
html += ' <div class="v6-fld"><label class="v6-lbl">Cijena po KM (EUR)</label><input id="ff_cijena_km" class="v6-inp v6-num" type="number" step="0.01" value="0.50" oninput="v6CalcCost()" /></div>';
html += ' <div class="v6-fld v6-w2"><label class="v6-lbl">💰 Trošak prijevoza (auto)</label><input id="ff_trosak_prijevoz" class="v6-inp v6-num v6-calc" type="number" step="0.01" readonly /></div>';
html += ' </div>';
html += ' </div>';
// OCR Attachments
html += ' <div class="v6-fs">';
html += ' <div class="v6-fs-t">Prilozi · OCR</div>';
html += ' <div class="v6-att-z" onclick="document.getElementById(\'v6FilePick\').click()">';
html += ' <input type="file" id="v6FilePick" accept=".pdf,.jpg,.jpeg,.png" multiple style="display:none" onchange="v6UploadPrilog(this)" />';
html += ' <div>Klikni ili dovuci PDF/JPG/PNG (cestarine, gorivo, parking, smještaj)</div>';
html += ' <div style="font-size:11px;color:#788798;margin-top:4px">AI OCR će automatski pročitati iznos, datum, dobavljača, OIB</div>';
html += ' </div>';
html += ' <div id="v6AttList" class="v6-att-l"></div>';
html += ' </div>';
// Costs grid
html += ' <div class="v6-fs">';
html += ' <div class="v6-fs-t">Troškovi (EUR)</div>';
html += ' <div class="v6-g4">';
html += ' <div class="v6-fld"><label class="v6-lbl">Cestarine</label><input id="ff_cestarine" class="v6-inp v6-num" type="number" step="0.01" value="0" oninput="v6CalcTotal()" /></div>';
html += ' <div class="v6-fld"><label class="v6-lbl">Parkirne</label><input id="ff_parkirne" class="v6-inp v6-num" type="number" step="0.01" value="0" oninput="v6CalcTotal()" /></div>';
html += ' <div class="v6-fld"><label class="v6-lbl">Gorivo</label><input id="ff_gorivo" class="v6-inp v6-num" type="number" step="0.01" value="0" oninput="v6CalcTotal()" /></div>';
html += ' <div class="v6-fld"><label class="v6-lbl">Smještaj</label><input id="ff_smjestaj" class="v6-inp v6-num" type="number" step="0.01" value="0" oninput="v6CalcTotal()" /></div>';
html += ' </div>';
html += ' <div class="v6-g4" style="margin-top:8px">';
html += ' <div class="v6-fld"><label class="v6-lbl">Ostali troškovi</label><input id="ff_ostali" class="v6-inp v6-num" type="number" step="0.01" value="0" oninput="v6CalcTotal()" /></div>';
html += ' <div class="v6-fld"><label class="v6-lbl">Broj dnevnica</label><input id="ff_dnevnice_n" class="v6-inp v6-num" type="number" value="0" oninput="v6CalcTotal()" /></div>';
html += ' <div class="v6-fld"><label class="v6-lbl">Iznos po dnevnici (EUR)</label><input id="ff_dnevnica_iznos" class="v6-inp v6-num" type="number" step="0.01" value="30" oninput="v6CalcTotal()" /></div>';
html += ' <div class="v6-fld"><label class="v6-lbl">💰 Ukupno dnevnice</label><input id="ff_dnevnice_uk" class="v6-inp v6-num v6-calc" type="number" step="0.01" readonly /></div>';
html += ' </div>';
html += ' <div class="v6-g3" style="margin-top:8px">';
html += ' <div class="v6-fld"><label class="v6-lbl">Predujam (EUR)</label><input id="ff_predujam" class="v6-inp v6-num" type="number" step="0.01" value="0" oninput="v6CalcTotal()" /></div>';
html += ' <div class="v6-fld v6-w2"><label class="v6-lbl">Napomena</label><textarea id="ff_napomena" class="v6-inp" rows="2"></textarea></div>';
html += ' </div>';
html += ' </div>';
// Totals footer
html += ' <div class="v6-tot">';
html += ' <div class="v6-tot-i"><div class="v6-lbl">Ukupni trošak</div><div class="v6-val" id="ff_uk_trosak">0,00 €</div></div>';
html += ' <div class="v6-tot-i"><div class="v6-lbl">Manje predujam</div><div class="v6-val" id="ff_minus_pred" style="color:#f0b429">0,00 €</div></div>';
html += ' <div class="v6-tot-i"><div class="v6-lbl">Za isplatu</div><div class="v6-val" id="ff_za_isplatu">0,00 €</div></div>';
html += ' </div>';
} else {
// GENERIC FORM — auto-grid 2 columns
html += '<div class="v6-fs"><div class="v6-g2">';
let col = 0;
fields.forEach(f => {
const span = colSpan(f);
const req = f.required ? ' req' : '';
const reqAttr = f.required ? ' required' : '';
const fieldClass = span === 2 ? 'v6-fld v6-w2' : 'v6-fld';
html += '<div class="' + fieldClass + '">';
html += '<label class="v6-lbl' + req + '">' + (f.label||f.name) + '</label>';
if (f.type === 'textarea') html += '<textarea id="ff_' + f.name + '" class="v6-inp" rows="3"' + reqAttr + '></textarea>';
else if (f.type === 'select') html += '<select id="ff_' + f.name + '" class="v6-inp"' + reqAttr + '><option></option>' + (f.options||[]).map(o => '<option>' + o + '</option>').join('') + '</select>';
else if (f.type === 'checkbox') html += '<input id="ff_' + f.name + '" type="checkbox" />';
else {
const numClass = (f.type === 'number' || (f.name && (f.name.includes('iznos')||f.name.includes('km')||f.name.includes('amount')))) ? ' v6-num' : '';
html += '<input id="ff_' + f.name + '" class="v6-inp' + numClass + '" type="' + (f.type||'text') + '"' + reqAttr + ' />';
}
html += '</div>';
});
html += '</div></div>';
}
// Footer buttons
html += '<div class="v6-fs" style="display:flex;gap:8px;justify-content:flex-end">';
html += ' <button class="v6-btn" onclick="document.getElementById(\'formRender\').innerHTML=\'\'">Zatvori</button>';
html += ' <button class="v6-btn" onclick="submitForm(\'' + code + '\',\'draft\')">Spremi draft</button>';
html += ' <button class="v6-btn primary" onclick="submitForm(\'' + code + '\',\'submitted\')">Pošalji</button>';
html += '</div>';
html += '<div id="formStatus" style="padding:10px 16px;font-size:13px"></div>';
html += '</div>';
document.getElementById('formRender').innerHTML = html;
document.getElementById('formRender').scrollIntoView({behavior:'smooth'});
if (isPutni) v6InitPutni();
} catch(e) { alert(e.message); }
}
// V6 PUTNI NALOG HELPERS
window.v6Attachments = window.v6Attachments || [];
function v6InitPutni() {
v6Attachments = [];
v6CalcKM(); v6CalcCost(); v6CalcTotal();
}
async function v6GradAuto(input, fieldKey) {
const q = input.value.trim();
const ac = document.getElementById('ac_' + fieldKey);
if (!q || q.length < 2) { ac.classList.remove('show'); return; }
try {
const r = await fetch('/sport/api/ai/gradovi?q=' + encodeURIComponent(q) + '&limit=10');
const list = await r.json();
if (!list.length) { ac.classList.remove('show'); return; }
ac.innerHTML = list.map(g => '<div onclick="document.getElementById(\'ff_' + fieldKey + '\').value=\'' + g.replace(/\'/g,"\\'") + '\';this.parentNode.classList.remove(\'show\');v6CalcKM()">' + g + '</div>').join('');
ac.classList.add('show');
} catch (e) { ac.classList.remove('show'); }
}
async function v6CalcKM() {
const od = (document.getElementById('ff_polaziste')||{}).value;
const dod = (document.getElementById('ff_odrediste')||{}).value;
const aiKm = document.getElementById('ff_ai_km');
const total = document.getElementById('ff_km_total');
const kmPre = parseFloat((document.getElementById('ff_km_pre')||{}).value || 0);
const kmPost = parseFloat((document.getElementById('ff_km_post')||{}).value || 0);
// Manual override: km_post - km_pre
if (kmPre > 0 && kmPost > kmPre) {
if (total) total.value = (kmPost - kmPre).toFixed(1);
document.getElementById('aiHint').innerHTML = '✓ Računamo iz stanja brzinomjera: ' + kmPre + ' → ' + kmPost + ' = ' + (kmPost - kmPre) + ' km';
} else if (od && dod && od !== dod) {
try {
const r = await fetch('/sport/api/ai/distance?od=' + encodeURIComponent(od) + '&do=' + encodeURIComponent(dod));
const d = await r.json();
if (d.found) {
if (aiKm) aiKm.value = d.udaljenost_km;
if (total) total.value = (d.udaljenost_km * 2).toFixed(1);
document.getElementById('aiHint').innerHTML = '🤖 AI: ' + od + ' → ' + dod + ' = ' + d.udaljenost_km + ' km × 2 (povratak) = ' + (d.udaljenost_km * 2) + ' km · vrijeme: ~' + d.vrijeme_minute + ' min · izvor: ' + d.izvor;
} else {
document.getElementById('aiHint').innerHTML = '⚠️ ' + d.suggestion + ' Unesi ručno KM stanje brzinomjera ili upiši ukupno.';
}
} catch(e) { document.getElementById('aiHint').innerHTML = '⚠️ AI greška: ' + e.message; }
}
v6CalcCost();
}
function v6CalcCost() {
const km = parseFloat((document.getElementById('ff_km_total')||{}).value || 0);
const cij = parseFloat((document.getElementById('ff_cijena_km')||{}).value || 0.50);
const t = document.getElementById('ff_trosak_prijevoz');
if (t) t.value = (km * cij).toFixed(2);
v6CalcTotal();
}
function v6CalcTotal() {
const f = id => parseFloat((document.getElementById(id)||{}).value || 0);
const trprij = f('ff_trosak_prijevoz');
const cest = f('ff_cestarine'); const park = f('ff_parkirne');
const gor = f('ff_gorivo'); const smj = f('ff_smjestaj'); const ost = f('ff_ostali');
const dn_n = f('ff_dnevnice_n'); const dn_iz = f('ff_dnevnica_iznos');
const pred = f('ff_predujam');
const dn_uk = dn_n * dn_iz;
const dn_uk_el = document.getElementById('ff_dnevnice_uk');
if (dn_uk_el) dn_uk_el.value = dn_uk.toFixed(2);
const ukupno = trprij + cest + park + gor + smj + ost + dn_uk;
const za_isp = ukupno - pred;
const fmt = n => n.toLocaleString('hr-HR', {minimumFractionDigits:2,maximumFractionDigits:2}) + ' €';
const uk_el = document.getElementById('ff_uk_trosak');
const mp_el = document.getElementById('ff_minus_pred');
const zi_el = document.getElementById('ff_za_isplatu');
if (uk_el) uk_el.textContent = fmt(ukupno);
if (mp_el) mp_el.textContent = '' + fmt(pred);
if (zi_el) zi_el.textContent = fmt(za_isp);
}
async function v6UploadPrilog(input) {
const files = input.files;
if (!files || !files.length) return;
const list = document.getElementById('v6AttList');
for (const file of files) {
const item = document.createElement('div');
item.className = 'v6-att-i';
item.innerHTML = '<span>📄 ' + file.name + '</span><span class="v6-tag">OCR...</span>';
list.appendChild(item);
const fd = new FormData();
fd.append('file', file);
fd.append('tip', 'racun');
try {
const r = await fetch('/sport/api/ai/ocr-prilog', {method:'POST', body: fd});
const d = await r.json();
const tag = d.ai_amount ? d.tip : 'parsed';
const amt = d.ai_amount ? (d.ai_amount.toFixed(2) + ' €') : '?';
const vendor = d.ai_vendor ? d.ai_vendor.slice(0, 30) : '';
item.innerHTML = '<span>📄 ' + file.name + '</span><span class="v6-tag">✓ ' + tag + '</span><span style="color:#788798;font-size:11px">' + vendor + '</span><span class="v6-amt">' + amt + '</span>'
+ '<select onchange="v6PrilogAssign(' + (v6Attachments.length) + ',this.value)" class="v6-inp" style="margin-left:auto;width:auto;font-size:11px">'
+ '<option value="">— pridruži —</option>'
+ '<option value="ff_cestarine">Cestarine</option>'
+ '<option value="ff_parkirne">Parkirne</option>'
+ '<option value="ff_gorivo">Gorivo</option>'
+ '<option value="ff_smjestaj">Smještaj</option>'
+ '<option value="ff_ostali">Ostali</option>'
+ '</select>';
v6Attachments.push({file: file.name, ocr: d, assigned_to: null});
} catch(e) {
item.innerHTML = '<span>📄 ' + file.name + '</span><span class="v6-tag" style="background:#6e2a2a">✗ greška</span>';
}
}
input.value = '';
}
function v6PrilogAssign(idx, fieldId) {
const att = v6Attachments[idx];
if (!att || !fieldId) return;
att.assigned_to = fieldId;
const el = document.getElementById(fieldId);
if (el && att.ocr.ai_amount) {
const cur = parseFloat(el.value || 0);
el.value = (cur + att.ocr.ai_amount).toFixed(2);
v6CalcTotal();
}
}
async function submitForm(code, status) {
const tpl = await v2Fetch('/forms/templates/'+code);
let fields = (tpl.schema_json && tpl.schema_json.fields) || [];
const data = {};
// Always read all standard fields by ID
fields.forEach(f => {
const el = document.getElementById('ff_'+f.name);
if (!el) return;
data[f.name] = f.type==='checkbox' ? el.checked : el.value;
});
// V6 putni nalog extra fields
const isPutni = code === 'obracun_putnih_troskova' || code === 'putni_nalog';
if (isPutni) {
['polaziste','odrediste','ai_km','datum_polaska','datum_povratka',
'km_pre','km_post','km_total','cijena_km','trosak_prijevoz',
'cestarine','parkirne','gorivo','smjestaj','ostali',
'dnevnice_n','dnevnica_iznos','dnevnice_uk','predujam','napomena'].forEach(k => {
const el = document.getElementById('ff_'+k);
if (el) data[k] = el.value;
});
// Attach OCR data
if (window.v6Attachments && v6Attachments.length) {
data._attachments = v6Attachments.map(a => ({
file: a.file, ocr: {amount: a.ocr.ai_amount, date: a.ocr.ai_date, vendor: a.ocr.ai_vendor, oib: a.ocr.ai_oib},
assigned_to: a.assigned_to
}));
}
}
try {
const d = await v2Fetch('/forms/submit', {method:'POST', body: JSON.stringify({template_code: code, data, status})});
document.getElementById('formStatus').innerHTML = '✅ Spremljeno · #'+d.submission_id+' · '+(d.reference_no||'');
} catch(e) { document.getElementById('formStatus').innerHTML = '❌ '+e.message; }
}
async function pageUsers() {
// GATE — moraš biti prijavljen
if (!state.v2Token || !state.v2User) {
document.getElementById('content').innerHTML = `
<div class="page-h"><h2>🔐 Korisnici i prava</h2><p class="muted">Za pristup ovoj stranici potrebna je prijava.</p></div>
<div class="card" style="max-width:480px;margin-top:18px;padding:24px;text-align:center">
<div style="font-size:48px;margin-bottom:8px">🔐</div>
<h3 style="margin-bottom:6px">Prijavi se za nastavak</h3>
<p class="muted" style="margin-bottom:16px">Modul "Korisnici" je dostupan samo prijavljenim administratorima.</p>
<button class="btn" onclick="showLogin('user')" style="min-width:160px">Prijavi se</button>
</div>`;
return;
}
setTopbar('Administracija', 'Korisnici i prava');
const me = state.v2User || {};
const isAdmin = ['super_admin','pgz_admin'].includes(me.user_type);
const isSuper = me.user_type === 'super_admin';
document.getElementById('content').innerHTML = `
<div class="page-h" style="margin-bottom:14px">
<h2>👥 Korisnici i prava</h2>
<p class="muted">Multi-tenant · ${isSuper?'super_admin':isAdmin?'pgz_admin':'pregled'} · klik na red → akcije</p>
</div>
${isAdmin ? `
<div class="card" style="margin-bottom:14px;padding:14px">
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
<input id="usrSearch" class="inp" placeholder="🔍 traži email / ime / prezime" style="flex:1;min-width:240px" oninput="usrSearch()">
<select id="usrFilterType" class="inp" onchange="usrSearch()">
<option value="">Svi tipovi</option>
<option value="super_admin">super_admin</option>
<option value="pgz_admin">pgz_admin</option>
<option value="pgz_user">pgz_user</option>
<option value="pgz_finance">pgz_finance</option>
<option value="pgz_zzjz">pgz_zzjz</option>
<option value="savez_admin">savez_admin</option>
<option value="klub_admin">klub_admin</option>
<option value="klub_user">klub_user</option>
<option value="klub_clan">klub_clan</option>
</select>
<button class="btn ri-btn-primary" onclick="usrShowCreate()"> Novi korisnik</button>
<button class="btn" onclick="usrShowAudit()">📋 Audit</button>
</div>
</div>` : ''}
<div class="card" style="overflow:auto">
<div id="usrList">Učitavam...</div>
</div>
<div class="modal-bg" id="usrModal" style="display:none">
<div class="modal" id="usrModalBody" style="min-width:420px;max-width:560px"></div>
</div>
`;
await usrLoadList();
}
let _usrSearchTimer = null;
function usrSearch() {
clearTimeout(_usrSearchTimer);
_usrSearchTimer = setTimeout(usrLoadList, 300);
}
async function usrLoadList() {
const q = (document.getElementById('usrSearch')||{}).value || '';
const ut = (document.getElementById('usrFilterType')||{}).value || '';
const params = new URLSearchParams();
if (q) params.set('q', q);
if (ut) params.set('user_type', ut);
params.set('limit', '100');
try {
const d = await api('/api/v2/users/list?' + params.toString());
const me = state.v2User || {};
const isAdmin = ['super_admin','pgz_admin'].includes(me.user_type);
const isSuper = me.user_type === 'super_admin';
if (!d.results || d.results.length === 0) {
document.getElementById('usrList').innerHTML = '<div class="muted" style="padding:24px;text-align:center">Nema korisnika u tvom dosegu.</div>';
return;
}
let html = `<div class="muted" style="font-size:11px;margin-bottom:8px">${d.count} / ${d.total} korisnika</div>`;
html += '<table class="ri-tbl ri-sortable"><thead><tr><th>#</th><th>Email</th><th>Ime i prezime</th><th>Tip</th><th>Klub/Savez</th><th>Status</th><th>Lock</th><th>Last login</th><th style="text-align:right">Akcije</th></tr></thead><tbody>';
for (const u of d.results) {
const locked = u.locked_until && new Date(u.locked_until) > new Date();
const failBadge = u.failed_login_count > 0 ? `<span class="risk-medium">${u.failed_login_count}</span>` : '';
const lockBadge = locked ? '<span class="risk-critical">LOCKED</span>' : (u.aktivan ? '<span class="risk-low">aktivan</span>' : '<span class="risk-high">isključen</span>');
const mustCh = u.must_change_pwd ? ' <span class="risk-medium" title="Mora promijeniti lozinku">!</span>' : '';
const llogin = u.last_login ? new Date(u.last_login).toLocaleDateString('hr-HR') : '<span class="muted">nikad</span>';
const klubLabel = u.klub_id ? `klub#${u.klub_id}` : (u.savez_id ? `savez#${u.savez_id}` : '-');
let actions = '';
if (isAdmin && u.id !== me.id) {
actions = `
<button class="btn ri-btn-ghost" onclick="usrShowEdit(${u.id})" style="padding:2px 6px;font-size:10px">edit</button>
<button class="btn ri-btn-ghost" onclick="usrResetPwd(${u.id}, '${(u.email||'').replace(/'/g,'')}')" style="padding:2px 6px;font-size:10px">reset pwd</button>
<button class="btn ri-btn-ghost" onclick="usrToggle(${u.id})" style="padding:2px 6px;font-size:10px">${u.aktivan?'isključi':'aktiviraj'}</button>
${locked ? `<button class="btn ri-btn-ghost" onclick="usrUnlock(${u.id})" style="padding:2px 6px;font-size:10px">unlock</button>` : ''}
${isSuper ? `<button class="btn ri-btn-ghost" onclick="usrImpersonate(${u.id})" style="padding:2px 6px;font-size:10px;color:var(--amber)">impersonate</button>` : ''}
<button class="btn ri-btn-ghost" onclick="usrShowAudit(${u.id})" style="padding:2px 6px;font-size:10px">audit</button>`;
} else if (u.id === me.id) {
actions = `<button class="btn ri-btn-ghost" onclick="usrShowEdit(${u.id})" style="padding:2px 6px;font-size:10px">edit (ja)</button>`;
}
html += `<tr>
<td class="mono">${u.id}</td>
<td><b>${u.email}</b>${mustCh}</td>
<td>${(u.ime||'')+' '+(u.prezime||'')}</td>
<td><span class="mono" style="font-size:10px;color:var(--text3)">${u.user_type||'?'}</span></td>
<td class="mono" style="font-size:10px">${klubLabel}</td>
<td>${lockBadge} ${failBadge}</td>
<td>${locked ? `<span class="muted" title="${u.locked_until}">do ${new Date(u.locked_until).toLocaleTimeString('hr-HR',{hour:'2-digit',minute:'2-digit'})}</span>` : '-'}</td>
<td>${llogin}</td>
<td style="text-align:right;white-space:nowrap">${actions}</td>
</tr>`;
}
html += '</tbody></table>';
document.getElementById('usrList').innerHTML = html;
} catch(e) {
document.getElementById('usrList').innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>';
}
}
function usrModal(html) {
const m = document.getElementById('usrModal');
const b = document.getElementById('usrModalBody');
if (!m || !b) return;
b.innerHTML = html;
m.style.display = 'flex';
}
function usrModalClose() {
const m = document.getElementById('usrModal');
if (m) m.style.display = 'none';
}
function usrShowCreate() {
usrModal(`
<h3 style="margin-bottom:14px"> Novi korisnik</h3>
<input id="ncEmail" class="inp" placeholder="email@pgz.hr" style="width:100%;margin-bottom:8px">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">
<input id="ncIme" class="inp" placeholder="Ime">
<input id="ncPrezime" class="inp" placeholder="Prezime">
</div>
<select id="ncType" class="inp" style="width:100%;margin-bottom:8px">
<option value="klub_user">klub_user</option>
<option value="klub_admin">klub_admin</option>
<option value="klub_clan">klub_clan</option>
<option value="savez_user">savez_user</option>
<option value="savez_admin">savez_admin</option>
<option value="pgz_user">pgz_user</option>
<option value="pgz_finance">pgz_finance</option>
<option value="pgz_zzjz">pgz_zzjz</option>
<option value="pgz_admin">pgz_admin</option>
</select>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">
<input id="ncKlub" class="inp" type="number" placeholder="klub_id">
<input id="ncSavez" class="inp" type="number" placeholder="savez_id">
</div>
<input id="ncTel" class="inp" placeholder="Telefon" style="width:100%;margin-bottom:8px">
<p class="muted" style="font-size:10px;margin-bottom:12px">Default lozinka: <code>PgzSport2026!</code> + must_change_pwd=true</p>
<div id="ncErr" class="banner crit" style="display:none;margin-bottom:8px"></div>
<div style="display:flex;gap:8px">
<button class="btn ri-btn-primary" style="flex:1" onclick="usrCreate()">Kreiraj</button>
<button class="btn" onclick="usrModalClose()">Odustani</button>
</div>
`);
}
async function usrCreate() {
const body = {
email: document.getElementById('ncEmail').value.trim(),
ime: document.getElementById('ncIme').value.trim() || null,
prezime: document.getElementById('ncPrezime').value.trim() || null,
user_type: document.getElementById('ncType').value,
klub_id: parseInt(document.getElementById('ncKlub').value) || null,
savez_id: parseInt(document.getElementById('ncSavez').value) || null,
telefon: document.getElementById('ncTel').value.trim() || null,
};
const err = document.getElementById('ncErr');
err.style.display = 'none';
if (!body.email) { err.textContent = 'Email je obavezan'; err.style.display='block'; return; }
try {
const d = await api('/api/v2/users/create', { method:'POST', body: JSON.stringify(body) });
alert('Kreiran user #' + d.id + (d.temporary_password ? '\nPrivremena lozinka: ' + d.temporary_password : ''));
usrModalClose();
await usrLoadList();
} catch(e) { err.textContent = e.message; err.style.display='block'; }
}
async function usrShowEdit(uid) {
try {
const d = await api('/api/v2/users/list?limit=200');
const u = (d.results || []).find(x => x.id === uid);
if (!u) { alert('Korisnik nije u tvom dosegu'); return; }
usrModal(`
<h3 style="margin-bottom:14px">✎ Edit #${u.id} · ${u.email}</h3>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">
<input id="eIme" class="inp" placeholder="Ime" value="${u.ime||''}">
<input id="ePrezime" class="inp" placeholder="Prezime" value="${u.prezime||''}">
</div>
<select id="eType" class="inp" style="width:100%;margin-bottom:8px">
${['super_admin','pgz_admin','pgz_user','pgz_finance','pgz_zzjz','savez_admin','savez_user','klub_admin','klub_user','klub_clan'].map(t=>`<option value="${t}" ${u.user_type===t?'selected':''}>${t}</option>`).join('')}
</select>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">
<input id="eKlub" class="inp" type="number" placeholder="klub_id" value="${u.klub_id||''}">
<input id="eSavez" class="inp" type="number" placeholder="savez_id" value="${u.savez_id||''}">
</div>
<input id="eTel" class="inp" placeholder="Telefon" value="${u.telefon||''}" style="width:100%;margin-bottom:8px">
<input id="eOib" class="inp" placeholder="OIB" value="${u.oib||''}" style="width:100%;margin-bottom:12px">
<div id="eErr" class="banner crit" style="display:none;margin-bottom:8px"></div>
<div style="display:flex;gap:8px">
<button class="btn ri-btn-primary" style="flex:1" onclick="usrEdit(${u.id})">Spremi</button>
<button class="btn" onclick="usrModalClose()">Odustani</button>
</div>
`);
} catch(e) { alert('Greška: '+e.message); }
}
async function usrEdit(uid) {
const body = {
ime: document.getElementById('eIme').value.trim() || null,
prezime: document.getElementById('ePrezime').value.trim() || null,
user_type: document.getElementById('eType').value,
klub_id: parseInt(document.getElementById('eKlub').value) || null,
savez_id: parseInt(document.getElementById('eSavez').value) || null,
telefon: document.getElementById('eTel').value.trim() || null,
oib: document.getElementById('eOib').value.trim() || null,
};
try {
await api('/api/v2/users/' + uid, { method:'PUT', body: JSON.stringify(body) });
usrModalClose();
await usrLoadList();
} catch(e) {
document.getElementById('eErr').textContent = e.message;
document.getElementById('eErr').style.display = 'block';
}
}
async function usrResetPwd(uid, email) {
if (!confirm('Resetirati lozinku korisnika ' + email + '?\nGenerirat će se nova privremena lozinka.')) return;
try {
const d = await api('/api/v2/users/' + uid + '/reset-password', { method:'POST' });
prompt('Nova privremena lozinka — kopiraj i pošalji korisniku:', d.temporary_password);
await usrLoadList();
} catch(e) { alert('Greška: ' + e.message); }
}
async function usrToggle(uid) {
try {
await api('/api/v2/users/' + uid + '/toggle-active', { method:'POST' });
await usrLoadList();
} catch(e) { alert('Greška: ' + e.message); }
}
async function usrUnlock(uid) {
try {
await api('/api/v2/users/' + uid + '/unlock', { method:'POST' });
await usrLoadList();
} catch(e) { alert('Greška: ' + e.message); }
}
async function usrImpersonate(uid) {
if (!confirm('Impersonate korisnika #' + uid + '?\nIzdaje se 2h token i ti gubiš svoju sesiju u ovom tabu.')) return;
try {
const d = await api('/api/v2/admin/impersonate', { method:'POST', body: JSON.stringify({target_user_id: uid}) });
localStorage.setItem('rinet_v2_token', d.token);
localStorage.setItem('rinet_v2_user', JSON.stringify(d.as_user));
alert('Sad si u ulozi: ' + d.as_user.email + '\nDo ' + new Date(d.expires_at).toLocaleString('hr-HR'));
await checkRole();
render();
} catch(e) { alert('Greška: ' + e.message); }
}
async function usrShowAudit(uid) {
try {
const url = uid ? '/api/v2/users/' + uid + '/audit?limit=100' : '/api/v2/admin/audit?limit=100';
const d = await api(url);
let html = '<h3 style="margin-bottom:10px">📋 Audit ' + (uid?`za #${uid}`:'(globalno)') + '</h3>';
if (!d.results.length) {
html += '<div class="muted" style="padding:14px">Nema zapisa.</div>';
} else {
html += '<table class="ri-tbl ri-sortable"><thead><tr><th>Datum</th><th>User</th><th>Akcija</th></tr></thead><tbody>';
for (const a of d.results) {
const dt = new Date(a.created_at).toLocaleString('hr-HR');
const who = a.email ? a.email : ('user#'+a.user_id);
html += `<tr><td class="mono" style="font-size:10px">${dt}</td><td>${who}</td><td class="mono" style="font-size:10px">${a.action||''}</td></tr>`;
}
html += '</tbody></table>';
}
html += '<div style="text-align:right;margin-top:14px"><button class="btn" onclick="usrModalClose()">Zatvori</button></div>';
usrModal(html);
} catch(e) { alert('Greška: ' + e.message); }
}
async function createUser() {
const body = {
email: document.getElementById('nuEmail').value,
full_name: document.getElementById('nuName').value,
password: document.getElementById('nuPwd').value,
role: document.getElementById('nuRole').value,
klub_id: parseInt(document.getElementById('nuKlub').value||'0')||null
};
try {
const d = await v2Fetch('/users', {method:'POST', body: JSON.stringify(body)});
document.getElementById('usrStatus').innerHTML = `✅ User #${d.user_id} kreiran`;
setTimeout(pageUsers, 800);
} catch(e) { document.getElementById('usrStatus').innerHTML = '❌ '+e.message; }
}
// V2 LOGIN BRIDGE — overlay on existing showLogin/doLogin if it uses old endpoint
window.v2Login = async function(email, pwd) {
const r = await fetch('/sport/api/v2/auth/login', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({email, password: pwd})});
const d = await r.json();
if (!r.ok) throw new Error(d.detail||'Login failed');
localStorage.setItem('rinet_v2_token', d.token);
localStorage.setItem('rinet_v2_user', JSON.stringify(d.user));
return d;
};
// ═══════════════════════════════════════════════════════
// KATEGORIJE auto-recalculate when sport/datum changes
// ═══════════════════════════════════════════════════════
async function spRecalcCats() {
const sport = (document.getElementById('spSport')||{}).value;
const dob = (document.getElementById('spDob')||{}).value;
const preview = document.getElementById('spKatPreview');
if (!preview) return;
if (!sport || !dob) {
preview.style.display = 'none';
return;
}
preview.style.display = 'block';
preview.innerHTML = '<div class="muted">Računam kategoriju…</div>';
try {
const r = await fetch('/sport/api/v2/dobne-kategorije/auto-assign', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({datum_rodenja: dob, sport: sport})
});
const d = await r.json();
if (d.warning) {
preview.innerHTML = `<div class="muted">⚠️ ${d.warning}</div>`;
return;
}
let html = `<div style="font-size:11px;color:var(--text2);margin-bottom:6px">DOB: <b>${d.starost} godina</b> · referentna sezona ${d.referentna_godina}</div>`;
if (d.primary) {
html += `<div style="margin-bottom:6px">
<span class="muted" style="font-size:10px">Glavna kategorija:</span>
<span style="display:inline-block;padding:3px 10px;background:var(--accent);color:white;border-radius:4px;font-weight:600;margin-left:6px">
${d.primary.oznaka} · ${d.primary.naziv}
</span>
<span class="muted" style="font-size:10px;margin-left:6px">(${d.primary.organizacija})</span>
</div>`;
}
if (d.additional && d.additional.length) {
html += `<div style="margin-bottom:6px"><span class="muted" style="font-size:10px">+ Pripadne:</span> ${
d.additional.map(k => `<span style="display:inline-block;padding:2px 8px;background:var(--bg4);border:1px solid var(--border2);border-radius:4px;margin:0 4px;font-size:11px">${k.oznaka}</span>`).join('')
}</div>`;
}
if (d.promocije && d.promocije.length) {
html += `<div style="margin-top:8px;padding-top:8px;border-top:1px solid var(--border)">
<div class="muted" style="font-size:11px;margin-bottom:6px">📈 Mogućnost promocije u stariju selekciju (mladi se često priključuju glavnoj ekipi):</div>`;
d.promocije.forEach(k => {
html += `<label style="display:block;margin:3px 0;cursor:pointer;font-size:12px">
<input type="checkbox" name="spPromo" value="${k.oznaka}" style="margin-right:6px">
<b>${k.oznaka}</b> · ${k.naziv} <span class="muted" style="font-size:10px">(${k.min_godina||'?'}-${k.max_godina||'∞'} god, ${k.organizacija})</span>
</label>`;
});
html += '</div>';
}
preview.innerHTML = html;
} catch(e) {
preview.innerHTML = `<div class="banner crit">Greška: ${e.message}</div>`;
}
}
// ═══════════════════════════════════════════════════════
// KATEGORIJE PAGE — pregled i statistika po sportu
// ═══════════════════════════════════════════════════════
async function pageKategorije() {
const c = document.getElementById('content');
setTopbar('Dobne kategorije', 'Pravila iz HR sportskih saveza · auto-asssign po dobi');
c.innerHTML = '<div class="card" style="padding:20px">Učitavam…</div>';
try {
const d = await fetch('/sport/api/v2/dobne-kategorije/by-sport').then(r=>r.json());
let html = `<div class="card" style="padding:14px;margin-bottom:14px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<div>
<h3 style="margin:0">Dobne kategorije po sportu</h3>
<div class="muted" style="font-size:11px;margin-top:4px">${d.count} sportova s definiranim kategorijama prema pravilima HR saveza</div>
</div>
<button class="btn" onclick="recalcAllCategories()">↻ Recalc svih sportaša</button>
</div>
</div>`;
for (const sp of d.results) {
html += `<div class="card" style="margin-bottom:10px">
<div style="padding:10px 14px;background:var(--bg3);border-bottom:1px solid var(--border);display:flex;justify-content:space-between">
<h4 style="margin:0;text-transform:capitalize">${sp.sport}</h4>
<span class="muted" style="font-size:11px">${sp.broj} kategorija</span>
</div>
<table class="ri-tbl ri-sortable" style="margin:0">
<thead><tr><th>Oznaka</th><th>Naziv</th><th>Dob</th><th>Organizacija</th><th>Napomena</th><th>Promocija</th></tr></thead>
<tbody>`;
for (const k of sp.kategorije) {
const dobRange = `${k.min_godina ?? 0}-${k.max_godina ?? '∞'}`;
const promo = k.promocija_dozvoljena ? '✓' : '—';
html += `<tr>
<td><span style="display:inline-block;padding:2px 8px;background:var(--accent);color:white;border-radius:4px;font-weight:600;font-size:11px">${k.oznaka||'-'}</span></td>
<td><b>${k.naziv}</b></td>
<td class="mono">${dobRange}</td>
<td class="muted">${k.organizacija||'-'}</td>
<td class="muted" style="font-size:10px">${k.napomena||''}</td>
<td style="text-align:center">${promo}</td>
</tr>`;
}
html += '</tbody></table></div>';
}
c.innerHTML = html;
} catch(e) { c.innerHTML = '<div class="banner crit">Greška: '+e.message+'</div>'; }
}
async function recalcAllCategories() {
if (!confirm('Pokrenuti recalc kategorija za sve sportaše? (može trajati ~20s)')) return;
try {
const r = await api('/api/v2/sportas/recalc-all-categories', { method:'POST' });
alert(`Updated: ${r.updated}, skipped: ${r.skipped_no_sport}, errors: ${r.errors}`);
} catch(e) { alert('Greška: ' + e.message); }
}
// ═══════════════════════════════════════════════════════
// DOKUMENTI / PRAVILNICI / ZAKONI — RAG + AI Legal Expert
// ═══════════════════════════════════════════════════════
let _dokState = { filter_razina:'', filter_vrsta:'', filter_organizacija:'', filter_sport:'', q:'' };
async function pageDokumenti() {
const c = document.getElementById('content');
setTopbar('Pravilnici i zakoni', 'Baza znanja zakona, pravilnika, statuta i programa za sport u PGŽ · AI legal expert');
c.innerHTML = `
<div class="card" style="padding:14px;margin-bottom:14px">
<h3 style="margin:0 0 10px 0">🤖 Hybrid AI Agent <span class="muted" style="font-size:11px;font-weight:400">— SQL (operativni podaci) + RAG (zakoni i pravilnici)</span></h3>
<div style="display:flex;gap:8px;margin-bottom:10px">
<input id="dokAskQ" class="inp" style="flex:1" placeholder="Npr.: Tko je trener HNK Rijeke? · Sportski objekti u Crikvenici? · Koje obveze ima klub po Zakonu o sportu?" />
<button class="btn" onclick="dokAsk()">🤖 Pitaj</button>
</div>
<div id="dokAskResult"></div>
</div>
<div class="card" style="padding:14px;margin-bottom:14px">
<h4 style="margin:0 0 10px 0">📚 Pretraga dokumenata (RAG vector search)</h4>
<div style="display:grid;grid-template-columns:2fr 1fr 1fr 1fr 1fr;gap:8px;margin-bottom:8px">
<input id="dokQ" class="inp" placeholder="Pretraga po sadržaju ili nazivu..." onkeydown="if(event.key==='Enter')dokSearch()" />
<select id="dokRazina" class="inp" onchange="dokLoadList()">
<option value="">— sve razine —</option>
<option>RH</option><option>EU</option><option>HOO</option>
<option>Savez</option><option>PGZ</option><option>Grad Rijeka</option>
</select>
<select id="dokVrsta" class="inp" onchange="dokLoadList()">
<option value="">— sve vrste —</option>
<option>zakon</option><option>pravilnik</option><option>pravilnik_savez</option>
<option>statut</option><option>strategija</option><option>program</option>
<option>plan</option><option>odluka</option><option>raspodjela</option>
<option>izvjestaj</option><option>natjecaj</option>
</select>
<select id="dokSport" class="inp" onchange="dokLoadList()">
<option value="">— svi sportovi —</option>
<option>nogomet</option><option>rukomet</option><option>košarka</option>
<option>odbojka</option><option>vaterpolo</option><option>plivanje</option>
<option>boćanje</option><option>tenis</option><option>stolni tenis</option>
<option>atletika</option><option>veslanje</option><option>jedriličarstvo</option>
<option>karate</option><option>judo</option><option>taekwondo</option>
<option>biciklizam</option><option>šah</option><option>lov</option>
</select>
<button class="btn" onclick="dokSearch()">🔎 Search</button>
</div>
<div id="dokSearchResult" style="margin-top:10px"></div>
</div>
<div id="dokListBox" class="card" style="padding:14px"></div>`;
dokLoadList();
}
async function dokLoadList() {
const r = document.getElementById('dokRazina').value;
const v = document.getElementById('dokVrsta').value;
const sp = document.getElementById('dokSport').value;
let url = '/sport/api/v2/dokumenti/list?limit=300';
if (r) url += '&razina=' + encodeURIComponent(r);
if (v) url += '&vrsta=' + encodeURIComponent(v);
if (sp) url += '&sport=' + encodeURIComponent(sp);
const box = document.getElementById('dokListBox');
box.innerHTML = '<div class="muted">Učitavam…</div>';
try {
const d = await fetch(url).then(x => x.json());
let html = `<div style="display:flex;justify-content:space-between;margin-bottom:10px">
<div><b>${d.count}</b> dokumenata</div>
<div class="muted" style="font-size:11px">${[r,v,sp].filter(Boolean).join(' · ') || 'svi filteri'}</div>
</div>`;
if (!d.results.length) {
html += '<div class="muted">Nema rezultata.</div>';
} else {
html += '<table class="ri-tbl ri-sortable" style="margin:0"><thead><tr><th>Razina</th><th>Vrsta</th><th>Naziv</th><th>Organizacija</th><th>Sport</th><th>Glasnik</th><th></th></tr></thead><tbody>';
for (const doc of d.results) {
const razinaCss = {
'RH':'background:#0066cc;color:white',
'EU':'background:#003399;color:white',
'HOO':'background:#fb923c;color:white',
'Savez':'background:#7c3aed;color:white',
'PGZ':'background:#10b981;color:white',
'Grad Rijeka':'background:#dc2626;color:white'
}[doc.razina] || 'background:var(--bg4);color:var(--text)';
html += `<tr>
<td><span style="display:inline-block;padding:2px 6px;border-radius:3px;font-size:10px;font-weight:600;${razinaCss}">${doc.razina||'-'}</span></td>
<td><span class="muted" style="font-size:10px">${doc.vrsta||'-'}</span></td>
<td><b style="cursor:pointer;color:var(--accent)" onclick="dokView(${doc.id})">${doc.naziv}</b>
<div class="muted" style="font-size:10px">${doc.kratak_opis||''}</div></td>
<td class="muted" style="font-size:11px">${doc.organizacija||'-'}</td>
<td class="muted" style="font-size:11px">${doc.sport||'-'}</td>
<td class="muted" style="font-size:10px">${doc.sluzbeni_glasnik||'-'}</td>
<td><div class="ri-actions">
<button class="ri-icon-btn-sm" onclick="dokView(${doc.id})" title="Pregled detalja">${iconEye()}</button>
<button class="ri-icon-btn-sm" onclick="window.open('/sport/api/v2/dokumenti/${doc.id}/pdf','_blank')" title="Pregled PDF-a">${iconFile()}</button>
<button class="ri-icon-btn-sm" onclick="window.open('/sport/api/v2/dokumenti/${doc.id}/text','_blank')" title="Parseni tekst">${iconDownload()}</button>
${doc.izvor_url ? `<a class="ri-icon-btn-sm" href="${doc.izvor_url}" target="_blank" title="Originalni izvor (web)">${iconExternal()}</a>` : ''}
</div></td>
</tr>`;
}
html += '</tbody></table>';
}
box.innerHTML = html;
} catch(e) { box.innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>'; }
}
async function dokSearch() {
const q = document.getElementById('dokQ').value.trim();
const r = document.getElementById('dokRazina').value || null;
const sp = document.getElementById('dokSport').value || null;
const box = document.getElementById('dokSearchResult');
if (!q) { box.innerHTML = ''; return; }
box.innerHTML = '<div class="muted">RAG vector search…</div>';
try {
const body = { q: q, limit: 8 };
if (r) body.razina = r;
if (sp) body.sport = sp;
const d = await fetch('/sport/api/v2/dokumenti/search', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify(body)
}).then(x => x.json());
if (!d.results || !d.results.length) {
box.innerHTML = '<div class="muted">Nema RAG rezultata.</div>'; return;
}
let html = `<div class="muted" style="font-size:11px;margin-bottom:8px">RAG vector search — ${d.count} pogodaka:</div>`;
for (const r of d.results) {
html += `<div style="padding:10px;margin-bottom:8px;border:1px solid var(--border);border-radius:6px;background:var(--bg3)">
<div style="display:flex;justify-content:space-between;margin-bottom:4px">
<b style="cursor:pointer;color:var(--accent)" onclick="dokView(${r.dokument_id})">${r.naziv}</b>
<span class="muted" style="font-size:10px">score ${r.score}</span>
</div>
<div style="font-size:11px;color:var(--text2);margin-bottom:6px">${r.razina||''} · ${r.organizacija||''} · ${r.vrsta||''} ${r.sport ? '· '+r.sport : ''}</div>
<div style="font-size:12px;color:var(--text);line-height:1.5">${r.snippet||''}</div>
${r.izvor_url ? `<a href="${r.izvor_url}" target="_blank" class="muted" style="font-size:10px;margin-top:6px;display:inline-block">↗ ${r.izvor_url}</a>` : ''}
</div>`;
}
box.innerHTML = html;
} catch(e) { box.innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>'; }
}
async function dokAsk() {
const q = document.getElementById('dokAskQ').value.trim();
const box = document.getElementById('dokAskResult');
if (!q) return;
box.innerHTML = '<div class="muted">🤖 Hybrid AI agent razmišlja… (SQL + RAG)</div>';
try {
const d = await fetch('/sport/api/v2/ai/ask', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({q: q})
}).then(x => x.json());
const modeColor = {'SQL':'#10b981','RAG':'#7c3aed','BOTH':'#f59e0b','sql_error':'#dc2626','error':'#dc2626'}[d.mode]||'var(--accent)';
let html = `<div style="padding:14px;background:var(--bg3);border:1px solid var(--border);border-radius:8px;margin-top:8px">
<div style="font-size:11px;color:var(--text2);margin-bottom:10px;display:flex;justify-content:space-between">
<span>🤖 ODGOVOR (Hybrid AI)</span>
<span><span style="display:inline-block;padding:2px 8px;border-radius:3px;background:${modeColor};color:white;font-weight:600">${d.mode||'?'}</span> ${d.sql_count!==undefined?`<span class="muted">${d.sql_count} retka</span>`:''}</span>
</div>
<div style="white-space:pre-wrap;line-height:1.6;font-size:13px;color:var(--text-bright)">${(d.answer||'').replace(/</g,'&lt;')}</div>
${d.sql ? `<details style="margin-top:10px"><summary class="muted" style="cursor:pointer;font-size:11px">▸ generated SQL</summary><pre style="font-size:10px;background:var(--bg);padding:8px;border-radius:3px;overflow-x:auto;color:var(--text2)">${d.sql.replace(/</g,'&lt;')}</pre></details>` : ''}`;
if (d.sources && d.sources.length) {
html += `<div style="margin-top:14px;padding-top:12px;border-top:1px solid var(--border)">
<div style="font-size:11px;color:var(--text2);margin-bottom:8px">📚 IZVORI (RAG retrieval):</div>`;
for (const src of d.sources) {
html += `<div style="margin:4px 0;font-size:11px">
<span style="display:inline-block;width:24px;color:var(--accent);font-weight:600">[${src.n}]</span>
<b style="cursor:pointer;color:var(--accent)" onclick="dokView(${src.dokument_id||''})">${src.naziv||'?'}</b>
<span class="muted">· ${src.razina||''} · ${src.organizacija||''} · score ${src.score}</span>
${src.izvor_url ? ` <a href="${src.izvor_url}" target="_blank" class="muted">↗</a>` : ''}
</div>`;
}
html += '</div>';
}
if (d.sql) {
html += `<div style="margin-top:14px;padding-top:12px;border-top:1px solid var(--border)">
<div class="muted" style="font-size:11px;margin-bottom:6px">📜 SQL (auto-generated):</div>
<pre style="background:var(--bg);padding:8px;border-radius:4px;font-size:10px;overflow-x:auto;color:var(--text2);margin:0">${d.sql}</pre>
</div>`;
}
html += '</div>';
box.innerHTML = html;
} catch(e) { box.innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>'; }
}
async function dokView(did) {
if (!did) return;
try {
const d = await fetch('/sport/api/v2/dokumenti/' + did).then(x => x.json());
const doc = d.dokument;
let html = `<div class="card" style="padding:18px;margin-bottom:14px">
<button class="btn" onclick="pageDokumenti()" style="margin-bottom:10px;padding:4px 10px;font-size:11px">← natrag</button>
<h2 style="margin:0 0 6px 0">${doc.naziv}</h2>
<div class="muted" style="font-size:11px;margin-bottom:14px">${doc.razina||''} · ${doc.organizacija||''} · ${doc.vrsta||''} ${doc.sport ? '· '+doc.sport : ''}</div>
${doc.kratak_opis ? `<div style="margin-bottom:14px;padding:10px;background:var(--bg3);border-radius:6px;font-size:13px">${doc.kratak_opis}</div>` : ''}
<table style="width:100%;font-size:12px">
${doc.sluzbeni_glasnik ? `<tr><td class="muted" style="width:160px">Službeni glasnik</td><td><b>${doc.sluzbeni_glasnik}</b></td></tr>` : ''}
${doc.izdano_datum ? `<tr><td class="muted">Izdano</td><td>${doc.izdano_datum}</td></tr>` : ''}
${doc.izvor_url ? `<tr><td class="muted">Izvor</td><td><a href="${doc.izvor_url}" target="_blank" class="muted">${doc.izvor_url}</a></td></tr>` : ''}
${doc.kljucne_rijeci && doc.kljucne_rijeci.length ? `<tr><td class="muted">Ključne riječi</td><td>${doc.kljucne_rijeci.map(k=>`<span style='display:inline-block;padding:2px 8px;background:var(--bg4);border-radius:3px;font-size:10px;margin:0 4px 4px 0'>${k}</span>`).join('')}</td></tr>` : ''}
</table>
</div>`;
if (doc.sadrzaj && doc.sadrzaj.length > 100) {
html += `<div class="card" style="padding:18px;margin-bottom:14px">
<h4 style="margin:0 0 10px 0">📄 Puni sadržaj</h4>
<div style="white-space:pre-wrap;font-size:12px;line-height:1.6;max-height:600px;overflow-y:auto">${doc.sadrzaj.replace(/</g,'&lt;')}</div>
</div>`;
}
if (d.chunks && d.chunks.length) {
html += `<div class="card" style="padding:18px">
<h4 style="margin:0 0 10px 0">🧩 Chunks (${d.chunks.length}) — embedded u Qdrant</h4>`;
for (const ch of d.chunks.slice(0,8)) {
html += `<div style="margin:8px 0;padding:8px;background:var(--bg3);border-radius:4px;font-size:11px;line-height:1.5">${(ch.chunk_text||'').slice(0,400)}…</div>`;
}
html += '</div>';
}
document.getElementById('content').innerHTML = html;
} catch(e) { alert('Greška: ' + e.message); }
}
// ═══════════════════════════════════════════════════════
// PGŽ BAZA — agregat zaboravljenih tablica
// ═══════════════════════════════════════════════════════
let _bazaTab = 'objekti';
async function pageBaza() {
setTopbar('PGŽ baza znanja', 'Objekti · Natjecanja · Manifestacije · Najbolji · Potpore · Statistika · Vijesti');
const c = document.getElementById('content');
c.innerHTML = `
<div class="card" style="padding:0;margin-bottom:14px;overflow:hidden">
<div style="display:flex;border-bottom:1px solid var(--border);background:var(--bg3)">
<button class="bazaTab" data-t="objekti" onclick="bazaSetTab('objekti')">🏟️ Objekti</button>
<button class="bazaTab" data-t="natjecanja" onclick="bazaSetTab('natjecanja')">🏆 Natjecanja</button>
<button class="bazaTab" data-t="manifestacije" onclick="bazaSetTab('manifestacije')">🎉 Manifestacije</button>
<button class="bazaTab" data-t="najbolji" onclick="bazaSetTab('najbolji')">⭐ Najbolji</button>
<button class="bazaTab" data-t="potpore" onclick="bazaSetTab('potpore')">💰 Potpore</button>
<button class="bazaTab" data-t="statistika" onclick="bazaSetTab('statistika')">📊 Statistika</button>
<button class="bazaTab" data-t="vijesti" onclick="bazaSetTab('vijesti')">📰 Vijesti</button>
<button class="bazaTab" data-t="suci" onclick="bazaSetTab('suci')">👨‍⚖️ Suci</button>
<button class="bazaTab" data-t="treneri" onclick="bazaSetTab('treneri')">🎽 Treneri</button>
<button class="bazaTab" data-t="sponzori" onclick="bazaSetTab('sponzori')">🤝 Sponzori</button>
<button class="bazaTab" data-t="mediji" onclick="bazaSetTab('mediji')">📺 Mediji</button>
<button class="bazaTab" data-t="akademski" onclick="bazaSetTab('akademski')">🎓 Akademski</button>
<button class="bazaTab" data-t="hoo" onclick="bazaSetTab('hoo')">🏅 HOO kategorizirani</button>
<button class="bazaTab" data-t="stats2025" onclick="bazaSetTab('stats2025')">📊 Stats 2025</button>
</div>
<style>.bazaTab{padding:10px 16px;background:transparent;color:var(--text2);border:none;cursor:pointer;font-size:12px;border-right:1px solid var(--border)}.bazaTab.act{background:var(--bg);color:var(--accent);font-weight:600;border-bottom:2px solid var(--accent)}</style>
</div>
<div id="bazaBody"></div>`;
bazaSetTab(_bazaTab);
}
async function bazaSetTab(t) {
_bazaTab = t;
document.querySelectorAll('.bazaTab').forEach(b => b.classList.toggle('act', b.dataset.t === t));
const box = document.getElementById('bazaBody');
if (!box) return;
box.innerHTML = '<div class="card" style="padding:14px"><div class="muted">Učitavam…</div></div>';
try {
if (t === 'objekti') await bazaObjekti(box);
else if (t === 'natjecanja') await bazaNatjecanja(box);
else if (t === 'manifestacije') await bazaManifestacije(box);
else if (t === 'najbolji') await bazaNajbolji(box);
else if (t === 'potpore') await bazaPotpore(box);
else if (t === 'statistika') await bazaStatistika(box);
else if (t === 'vijesti') await bazaVijesti(box);
else if (t === 'suci') await bazaSuci(box);
else if (t === 'treneri') await bazaTreneri(box);
else if (t === 'sponzori') await bazaSponzori(box);
else if (t === 'mediji') await bazaMediji(box);
else if (t === 'akademski') await bazaAkademski(box);
else if (t === 'hoo') await bazaKategorizirani(box);
else if (t === 'stats2025') await bazaStats2025(box);
} catch(e) { box.innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>'; }
}
async function bazaObjekti(box) {
const d = await fetch('/sport/api/v2/objekti/list').then(r => r.json());
const grouped = {};
for (const o of d.results) (grouped[o.grad||'-'] = grouped[o.grad||'-']||[]).push(o);
let html = `<div class="card" style="padding:14px"><h3 style="margin:0 0 10px 0">🏟️ Sportski objekti PGŽ <span class="muted" style="font-size:11px">${d.count} objekata u ${Object.keys(grouped).length} gradova</span></h3>`;
for (const grad of Object.keys(grouped).sort()) {
html += `<div style="margin:14px 0"><h4 style="margin:0 0 6px 0;color:var(--accent)">${grad} <span class="muted" style="font-size:11px;font-weight:400">(${grouped[grad].length})</span></h4>`;
html += '<table class="ri-tbl ri-sortable" style="margin:0"><thead><tr><th>Naziv</th><th>Tip</th><th>Adresa</th><th>Upravitelj</th><th>Kapacitet</th><th>Sportovi</th><th>God</th><th>Linkovi</th></tr></thead><tbody>';
for (const o of grouped[grad]) {
const mapsQ = encodeURIComponent((o.naziv||'') + ' ' + (o.adresa||'') + ' ' + (o.grad||'') + ' Hrvatska');
const mapsURL = (o.lat && o.lng) ? `https://www.google.com/maps/search/?api=1&query=${o.lat},${o.lng}` : `https://www.google.com/maps/search/?api=1&query=${mapsQ}`;
const sportsHtml = (o.sportovi||[]).map(sp => `<span class="badge" style="cursor:pointer;background:var(--bg4);padding:1px 5px;margin:1px;border-radius:3px;font-size:10px" onclick="event.stopPropagation();state.searchQ='${sp.replace(/'/g,'')} ${o.grad||''}';state.page='search';render()">${sp}</span>`).join(' ');
html += `<tr style="cursor:pointer" onclick="window.open('${mapsURL}','_blank')" title="Klik = otvori na Google Maps">
<td><a href="${mapsURL}" target="_blank" style="color:var(--accent);text-decoration:none;font-weight:600" onclick="event.stopPropagation()">📍 ${o.naziv}</a></td>
<td><span class="badge" style="display:inline-block;padding:2px 6px;background:var(--bg4);border-radius:3px;font-size:10px;cursor:pointer" onclick="event.stopPropagation();state.searchQ='${(o.tip||'').replace(/'/g,'')} sportski objekt';state.page='search';render()">${o.tip}</span></td>
<td class="muted" style="font-size:11px"><a href="${mapsURL}" target="_blank" class="muted" onclick="event.stopPropagation()">${o.adresa||'-'}</a></td>
<td class="muted" style="font-size:11px;cursor:pointer" onclick="event.stopPropagation();state.searchQ='${(o.upravitelj||'').replace(/'/g,'')}';state.page='search';render()">${o.upravitelj||'-'}</td>
<td class="mono">${o.kapacitet ? Number(o.kapacitet).toLocaleString() : '-'}</td>
<td style="font-size:10px">${sportsHtml}</td>
<td class="mono">${o.izgradeno||'-'}</td>
<td onclick="event.stopPropagation()">${o.web ? `<a href="${o.web}" target="_blank" class="muted" title="Web">🌐</a> ` : ''}<a href="${mapsURL}" target="_blank" title="Google Maps">🗺️</a></td></tr>`;
}
html += '</tbody></table></div>';
}
html += '</div>';
box.innerHTML = html;
}
async function bazaNatjecanja(box) {
const d = await fetch('/sport/api/v2/natjecanja/list?limit=300').then(r => r.json());
const bySport = {};
for (const n of d.results) (bySport[n.sport||'-'] = bySport[n.sport||'-']||[]).push(n);
let html = `<div class="card" style="padding:14px"><h3 style="margin:0 0 10px 0">🏆 Natjecanja <span class="muted" style="font-size:11px">${d.count} natjecanja</span></h3>`;
for (const sport of Object.keys(bySport).sort()) {
html += `<div style="margin:10px 0"><h4 style="margin:0 0 6px 0">${sport} <span class="muted" style="font-size:11px;font-weight:400">(${bySport[sport].length})</span></h4>`;
html += '<table class="ri-tbl ri-sortable" style="margin:0"><thead><tr><th>Naziv</th><th>Razina</th><th>Tip</th><th>Sezona</th><th>Kategorija</th><th>Početak</th><th>Status</th></tr></thead><tbody>';
for (const n of bySport[sport].slice(0, 30)) {
const safeNaz = (n.naziv||'').replace(/'/g,'');
const linkNaz = n.external_url ? `<a href="${n.external_url}" target="_blank" style="color:var(--accent);text-decoration:none"><b>${n.naziv}</b></a>` : `<b style="cursor:pointer;color:var(--accent)" onclick="state.searchQ='${safeNaz}';state.page='search';render()">${n.naziv}</b>`;
html += `<tr>
<td>${linkNaz}</td>
<td class="muted" style="font-size:11px">${n.razina||'-'}</td>
<td class="muted" style="font-size:11px">${n.tip||'-'}</td>
<td class="mono" style="font-size:11px">${n.sezona||'-'}</td>
<td class="muted" style="font-size:10px">${n.kategorija||'-'}</td>
<td class="mono" style="font-size:11px">${n.datum_pocetka||'-'}</td>
<td><span style="display:inline-block;padding:2px 6px;border-radius:3px;font-size:10px;background:${n.status==='aktivno'?'#10b981':'#fb923c'};color:white">${n.status||'-'}</span></td></tr>`;
}
html += '</tbody></table></div>';
}
html += '</div>';
box.innerHTML = html;
}
async function bazaManifestacije(box) {
const d = await fetch('/sport/api/v2/manifestacije/list').then(r => r.json());
let html = `<div class="card" style="padding:14px"><h3 style="margin:0 0 10px 0">🎉 Manifestacije <span class="muted" style="font-size:11px">${d.count} manifestacija</span></h3>`;
html += '<table class="ri-tbl ri-sortable" style="margin:0"><thead><tr><th>Naziv</th><th>Mjesto</th><th>Organizator</th><th>Razina</th><th>Sudionici</th><th>Od godine</th><th>Savez</th><th>Maps</th></tr></thead><tbody>';
for (const m of d.results) {
const safeNaziv = (m.naziv||'').replace(/'/g,'');
const safeMjesto = (m.mjesto||'').replace(/'/g,'');
const safeSavez = (m.savez_naziv||'').replace(/'/g,'');
const safeOrg = (m.organizator||'').replace(/'/g,'');
const mapsQ = encodeURIComponent(safeMjesto + ' Hrvatska');
const mapsURL = `https://www.google.com/maps/search/?api=1&query=${mapsQ}`;
const searchURL = `https://www.google.com/search?q=${encodeURIComponent(safeNaziv + ' ' + safeMjesto)}`;
html += `<tr>
<td><a href="${searchURL}" target="_blank" style="color:var(--accent);text-decoration:none;font-weight:600" title="Pretraži manifestaciju">${m.naziv}</a></td>
<td><a href="${mapsURL}" target="_blank" class="muted" title="Otvori na Google Maps">📍 ${m.mjesto||'-'}</a></td>
<td class="muted" style="font-size:11px;cursor:pointer" onclick="state.searchQ='${safeOrg}';state.page='search';render()">${m.organizator||'-'}</td>
<td><span class="badge" style="display:inline-block;padding:2px 6px;background:var(--bg4);border-radius:3px;font-size:10px;cursor:pointer" onclick="state.searchQ='manifestacije ${(m.razina||'').replace(/'/g,'')}';state.page='search';render()">${m.razina||'-'}</span></td>
<td class="muted">${m.broj_ucesnika||'-'}</td>
<td class="mono">${m.godina_od||'-'}</td>
<td class="muted" style="font-size:11px;cursor:pointer" onclick="if(${m.savez_id||0}){state.searchQ='${safeSavez}';state.page='savezi';render()}else{state.searchQ='${safeSavez}';state.page='search';render()}">${m.savez_naziv ? `<span style="color:var(--accent)">${m.savez_naziv}</span>` : '-'}</td>
<td><a href="${mapsURL}" target="_blank" title="Google Maps">🗺️</a> <a href="${searchURL}" target="_blank" title="Web pretraga">🔍</a></td></tr>`;
}
html += '</tbody></table></div>';
box.innerHTML = html;
}
async function bazaNajbolji(box) {
const d = await fetch('/sport/api/v2/najbolji/list').then(r => r.json());
const byGod = {};
for (const n of d.results) (byGod[n.godina] = byGod[n.godina]||[]).push(n);
let html = `<div class="card" style="padding:14px"><h3 style="margin:0 0 10px 0">⭐ Najbolji sportaši PGŽ <span class="muted" style="font-size:11px">${d.count} priznanja kroz godine</span></h3>`;
for (const god of Object.keys(byGod).sort().reverse()) {
html += `<div style="margin:14px 0"><h4 style="margin:0 0 6px 0;color:var(--accent)">${god}</h4>`;
html += '<table class="ri-tbl ri-sortable" style="margin:0"><thead><tr><th>Kategorija</th><th>Ime</th><th>Klub</th><th>Sport</th></tr></thead><tbody>';
for (const n of byGod[god]) {
const safeIme = (n.ime_prezime||'').replace(/'/g,'');
const safeKlub = (n.klub||'').replace(/'/g,'');
const safeSport = (n.sport||'').replace(/'/g,'');
html += `<tr>
<td><b>${n.kategorija}</b></td>
<td style="cursor:pointer;color:var(--accent)" onclick="state.searchQ='${safeIme}';state.page='search';render()">${n.ime_prezime||'-'}</td>
<td class="muted" style="cursor:pointer" onclick="state.searchQ='${safeKlub}';state.page='search';render()">${n.klub||'-'}</td>
<td class="muted" style="font-size:11px;cursor:pointer" onclick="state.searchQ='${safeSport} PGŽ';state.page='search';render()">${n.sport||'-'}</td></tr>`;
}
html += '</tbody></table></div>';
}
html += '</div>';
box.innerHTML = html;
}
async function bazaPotpore(box) {
const summary = await fetch('/sport/api/v2/potpore/by-godina').then(r => r.json());
const all = await fetch('/sport/api/v2/potpore/list').then(r => r.json());
let html = `<div class="card" style="padding:14px"><h3 style="margin:0 0 10px 0">💰 Potpore nositeljima kvalitete <span class="muted" style="font-size:11px">${all.count} isplata · ${all.total_iznos.toLocaleString()} EUR ukupno</span></h3>`;
html += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin-bottom:14px">';
for (const s of summary.results) {
html += `<div style="padding:10px;background:var(--bg3);border:1px solid var(--border);border-radius:6px;text-align:center">
<div class="muted" style="font-size:11px">${s.godina}</div>
<div style="font-size:18px;font-weight:700;color:var(--accent)">${Number(s.ukupno).toLocaleString()} €</div>
<div class="muted" style="font-size:10px">${s.broj} potpora</div></div>`;
}
html += '</div>';
html += '<table class="ri-tbl ri-sortable" style="margin:0"><thead><tr><th>Godina</th><th>Klub</th><th>Sport</th><th>Iznos</th><th>Napomena</th></tr></thead><tbody>';
for (const p of all.results) {
const safeKlub = (p.naziv_kluba||'').replace(/'/g,'');
const safeSport = (p.sport||'').replace(/'/g,'');
html += `<tr>
<td class="mono">${p.godina}</td>
<td style="cursor:pointer" onclick="state.searchQ='${safeKlub}';state.page='search';render()"><b style="color:var(--accent)">${p.naziv_kluba}</b></td>
<td class="muted" style="font-size:11px;cursor:pointer" onclick="state.searchQ='${safeSport} klubovi PGŽ';state.page='search';render()">${p.sport||'-'}</td>
<td class="mono" style="text-align:right;font-weight:600">${Number(p.iznos).toLocaleString()} €</td>
<td class="muted" style="font-size:10px">${p.napomena||''}</td></tr>`;
}
html += '</tbody></table></div>';
box.innerHTML = html;
}
async function bazaStatistika(box) {
const d = await fetch('/sport/api/v2/statistika/list?godina=2024').then(r => r.json());
let html = `<div class="card" style="padding:14px"><h3 style="margin:0 0 10px 0">📊 Statistika saveza 2024 <span class="muted" style="font-size:11px">${d.count} saveza</span></h3>`;
html += '<table class="ri-tbl ri-sortable" style="margin:0"><thead><tr><th>Savez</th><th>Klubova članica</th><th>Kategoriziranih</th><th>Registriranih</th><th>Rekreativaca</th><th>Trenera</th><th>Reprezent.</th><th>Stipend.</th></tr></thead><tbody>';
for (const r of d.results) {
const safeSavez = (r.savez_naziv||'').replace(/'/g,'');
html += `<tr style="cursor:pointer" onclick="state.searchQ='${safeSavez}';state.page='savezi';render()">
<td><b style="color:var(--accent)">${r.savez_naziv}</b></td>
<td class="mono" style="text-align:right">${r.klubova_clanica}</td>
<td class="mono" style="text-align:right">${r.kategoriziranih}</td>
<td class="mono" style="text-align:right">${r.registriranih}</td>
<td class="mono" style="text-align:right">${r.rekreativaca}</td>
<td class="mono" style="text-align:right">${r.trenera}</td>
<td class="mono" style="text-align:right">${r.reprezentativaca}</td>
<td class="mono" style="text-align:right">${r.stipendiranih}</td></tr>`;
}
html += '</tbody></table></div>';
box.innerHTML = html;
}
async function bazaKategorizirani(box) {
const d = await fetch('/sport/api/v2/kategorizirani/list').then(r=>r.json());
const byKat = {};
for (const x of d.results) (byKat[x.hoo_kategorija]=byKat[x.hoo_kategorija]||[]).push(x);
let h = `<div class="card" style="padding:14px"><h3>🏅 HOO kategorizirani sportaši PGŽ <span class="muted" style="font-size:11px">${d.count} sportaša · izvor: Sportski godišnjak ZS PGŽ 2025</span></h3>`;
const labels = {'I':'I (vrhunski svjetski)','II':'II (međunarodni)','III':'III (državni)','IV':'IV (mladi)','V':'V (perspektivni)','VI':'VI (lokalni)'};
const colors = {'I':'#dc2626','II':'#fb923c','III':'#a855f7','IV':'#0ea5e9','V':'#10b981','VI':'#6b7280'};
for (const kat of Object.keys(byKat).sort()) {
h += `<div style="margin:14px 0">
<h4 style="margin:0 0 8px 0;color:${colors[kat]||'var(--accent)'}">${labels[kat]||kat} <span class="muted" style="font-size:11px;font-weight:400">(${byKat[kat].length})</span></h4>`;
h += '<table class="ri-tbl ri-sortable"><thead><tr><th>Ime</th><th>Sport</th><th>Klub</th><th>Mjesto</th><th>Vrijedi</th></tr></thead><tbody>';
for (const x of byKat[kat]) {
const safeImePrez = ((x.ime||'')+' '+(x.prezime||'')).replace(/'/g,'').trim();
const safeKlub = (x.klub_naziv||'').replace(/'/g,'');
const safeSport = (x.sport||'').replace(/'/g,'');
const safeMjesto = (x.mjesto_rodenja||'').replace(/'/g,'');
const mapsQ = encodeURIComponent(safeMjesto + ' Hrvatska');
h += `<tr>
<td style="cursor:pointer" onclick="state.searchQ='${safeImePrez}';state.page='search';render()"><b style="color:var(--accent)">${x.ime} ${x.prezime||''}</b></td>
<td><span class="badge" style="display:inline-block;padding:2px 6px;background:var(--bg4);border-radius:3px;font-size:10px;cursor:pointer" onclick="state.searchQ='${safeSport} PGŽ klubovi';state.page='search';render()">${x.sport||'-'}</span></td>
<td style="cursor:pointer" onclick="state.searchQ='${safeKlub}';state.page='search';render()">${x.klub_naziv||'-'}</td>
<td class="muted">${x.mjesto_rodenja ? `<a href="https://www.google.com/maps/search/?api=1&query=${mapsQ}" target="_blank" class="muted">📍 ${x.mjesto_rodenja}</a>` : '-'}</td>
<td class="muted" style="font-size:10px">${x.hoo_kategorija_od||''} - ${x.hoo_kategorija_do||''}</td>
</tr>`;
}
h += '</tbody></table></div>';
}
h += '</div>';
box.innerHTML = h;
}
async function bazaStats2025(box) {
const d = await fetch('/sport/api/v2/statistika-2025').then(r=>r.json());
let h = `<div class="card" style="padding:14px">
<h3>📊 Sportaši PGŽ 2025 po savezu <span class="muted" style="font-size:11px">${d.izvor}</span></h3>
<div style="margin:10px 0;font-size:11px;color:var(--text2)">Ukupno: <b>${d.ukupno}</b> sportaša pregledano u sportskoj ambulanti 2025.</div>`;
h += '<table class="ri-tbl ri-sortable"><thead><tr><th>Savez</th><th style="text-align:right">Registriranih</th><th>Bar</th></tr></thead><tbody>';
const max = Math.max(...d.results.map(r=>r.registriranih));
for (const r of d.results) {
const w = Math.round(r.registriranih / max * 100);
h += `<tr>
<td><b>${r.savez}</b></td>
<td class="mono" style="text-align:right;font-weight:600">${r.registriranih.toLocaleString()}</td>
<td><div style="width:200px;height:14px;background:var(--bg4);border-radius:4px"><div style="width:${w}%;height:100%;background:var(--accent);border-radius:4px"></div></div></td>
</tr>`;
}
h += '</tbody></table></div>';
box.innerHTML = h;
}
async function bazaSuci(box) {
const d = await fetch('/sport/api/v2/suci/list').then(r=>r.json());
const bySport = {};
for (const x of d.results) (bySport[x.sport]=bySport[x.sport]||[]).push(x);
let h = `<div class="card" style="padding:14px"><h3>👨‍⚖️ Suci PGŽ <span class="muted" style="font-size:11px">${d.count} sudaca u ${Object.keys(bySport).length} sportova</span></h3>`;
for (const sp of Object.keys(bySport).sort()) {
h += `<div style="margin:10px 0"><h4 style="margin:0 0 6px 0;color:var(--accent)">${sp} <span class="muted" style="font-size:11px;font-weight:400">(${bySport[sp].length})</span></h4>`;
h += '<table class="ri-tbl ri-sortable"><thead><tr><th>Ime</th><th>Licenca</th><th>Razina</th><th>Org</th><th>Grad</th></tr></thead><tbody>';
for (const x of bySport[sp]) {
const safeIP = ((x.ime||'')+' '+(x.prezime||'')).replace(/'/g,'').trim();
const safeOrg = (x.organizacija||'').replace(/'/g,'');
const safeGrad = (x.grad||'').replace(/'/g,'');
const mapsQ = encodeURIComponent(safeGrad + ' Hrvatska');
h += `<tr>
<td style="cursor:pointer" onclick="state.searchQ='${safeIP}';state.page='search';render()"><b style="color:var(--accent)">${x.ime} ${x.prezime||''}</b></td>
<td><span style="display:inline-block;padding:2px 6px;background:var(--bg4);border-radius:3px;font-size:10px">${x.licenca||'-'}</span></td>
<td class="muted" style="font-size:11px">${x.kategorija||'-'}</td>
<td class="muted" style="font-size:11px;cursor:pointer" onclick="state.searchQ='${safeOrg}';state.page='search';render()">${x.organizacija||'-'}</td>
<td class="muted">${x.grad ? `<a href="https://www.google.com/maps/search/?api=1&query=${mapsQ}" target="_blank" class="muted">📍 ${x.grad}</a>` : '-'}</td></tr>`;
}
h += '</tbody></table></div>';
}
h += '</div>';
box.innerHTML = h;
}
async function bazaTreneri(box) {
const d = await fetch('/sport/api/v2/treneri/list').then(r=>r.json());
let h = `<div class="card" style="padding:14px"><h3>🎽 Treneri PGŽ <span class="muted" style="font-size:11px">${d.count} trenera</span></h3>`;
h += '<table class="ri-tbl ri-sortable"><thead><tr><th>Sport</th><th>Ime</th><th>Klub</th><th>Pozicija</th><th>Licenca</th><th>Grad</th></tr></thead><tbody>';
for (const x of d.results) {
const safeIP = ((x.ime||'')+' '+(x.prezime||'')).replace(/'/g,'').trim();
const safeKlub = (x.klub_naziv||'').replace(/'/g,'');
const safeSport = (x.sport||'').replace(/'/g,'');
const safeGrad = (x.grad||'').replace(/'/g,'');
const mapsQ = encodeURIComponent(safeGrad + ' Hrvatska');
h += `<tr>
<td><span class="badge" style="display:inline-block;padding:2px 6px;background:var(--bg4);border-radius:3px;font-size:10px;cursor:pointer" onclick="state.searchQ='${safeSport} treneri PGŽ';state.page='search';render()">${x.sport}</span></td>
<td style="cursor:pointer" onclick="state.searchQ='${safeIP}';state.page='search';render()"><b style="color:var(--accent)">${x.ime} ${x.prezime||''}</b></td>
<td style="cursor:pointer" onclick="state.searchQ='${safeKlub}';state.page='search';render()">${x.klub_naziv||'-'}</td>
<td class="muted" style="font-size:11px">${x.pozicija||'-'}</td>
<td class="muted" style="font-size:11px">${x.licenca||'-'}</td>
<td class="muted">${x.grad ? `<a href="https://www.google.com/maps/search/?api=1&query=${mapsQ}" target="_blank" class="muted">📍 ${x.grad}</a>` : '-'}</td></tr>`;
}
h += '</tbody></table></div>';
box.innerHTML = h;
}
async function bazaSponzori(box) {
const d = await fetch('/sport/api/v2/sponzori/list').then(r=>r.json());
const byKlub = {};
for (const x of d.results) (byKlub[x.naziv_kluba]=byKlub[x.naziv_kluba]||[]).push(x);
let h = `<div class="card" style="padding:14px"><h3>🤝 Sponzori PGŽ <span class="muted" style="font-size:11px">${d.count} sponzorskih ugovora · ${Object.keys(byKlub).length} klubova</span></h3>`;
for (const klub of Object.keys(byKlub).sort()) {
h += `<div style="margin:10px 0"><h4 style="margin:0 0 6px 0;color:var(--accent)">${klub}</h4>`;
h += '<table class="ri-tbl ri-sortable"><thead><tr><th>Sponzor</th><th>Tip</th><th>Od</th><th>Iznos</th><th>Napomena</th></tr></thead><tbody>';
for (const x of byKlub[klub]) {
const safeSpon = (x.sponzor||'').replace(/'/g,'');
const searchURL = `https://www.google.com/search?q=${encodeURIComponent(safeSpon + ' Hrvatska')}`;
h += `<tr>
<td><a href="${searchURL}" target="_blank" style="color:var(--accent);text-decoration:none"><b>${x.sponzor}</b></a></td>
<td><span style="display:inline-block;padding:2px 6px;background:var(--bg4);border-radius:3px;font-size:10px">${x.tip||'-'}</span></td>
<td class="mono" style="font-size:11px">${x.razdoblje_od||'-'}</td>
<td class="mono">${x.iznos_eur ? Number(x.iznos_eur).toLocaleString()+' €' : '-'}</td>
<td class="muted" style="font-size:10px">${x.napomena||''}</td></tr>`;
}
h += '</tbody></table></div>';
}
h += '</div>';
box.innerHTML = h;
}
async function bazaMediji(box) {
const d = await fetch('/sport/api/v2/mediji/list').then(r=>r.json());
const byTip = {};
for (const x of d.results) (byTip[x.tip]=byTip[x.tip]||[]).push(x);
let h = `<div class="card" style="padding:14px"><h3>📺 Sportski mediji PGŽ <span class="muted" style="font-size:11px">${d.count} medija</span></h3>`;
for (const tip of Object.keys(byTip).sort()) {
h += `<div style="margin:10px 0"><h4 style="margin:0 0 6px 0;color:var(--accent)">${tip} <span class="muted" style="font-size:11px;font-weight:400">(${byTip[tip].length})</span></h4>`;
h += '<table class="ri-tbl ri-sortable"><thead><tr><th>Naziv</th><th>Grad</th><th>Vlasnik</th><th>Pokrivenost</th><th>Sport</th><th>Web</th></tr></thead><tbody>';
for (const x of byTip[tip]) {
const safeNaz = (x.naziv||'').replace(/'/g,'');
const safeGrad = (x.grad||'').replace(/'/g,'');
const safeVlas = (x.vlasnik||'').replace(/'/g,'');
const mapsQ = encodeURIComponent(safeGrad + ' Hrvatska');
const titleEl = x.web ? `<a href="${x.web}" target="_blank" style="color:var(--accent);text-decoration:none"><b>${x.naziv}</b></a>` : `<b style="cursor:pointer;color:var(--accent)" onclick="state.searchQ='${safeNaz}';state.page='search';render()">${x.naziv}</b>`;
h += `<tr>
<td>${titleEl}</td>
<td>${x.grad ? `<a href="https://www.google.com/maps/search/?api=1&query=${mapsQ}" target="_blank" class="muted">📍 ${x.grad}</a>` : '-'}</td>
<td class="muted" style="font-size:11px;cursor:pointer" onclick="state.searchQ='${safeVlas}';state.page='search';render()">${x.vlasnik||'-'}</td>
<td><span style="display:inline-block;padding:2px 6px;background:var(--bg4);border-radius:3px;font-size:10px">${x.pokrivenost||'-'}</span></td>
<td class="muted" style="font-size:11px">${(x.sport_fokus||[]).join(', ')}</td>
<td>${x.web ? `<a href="${x.web}" target="_blank" class="muted">↗</a>` : ''}</td></tr>`;
}
h += '</tbody></table></div>';
}
h += '</div>';
box.innerHTML = h;
}
async function bazaAkademski(box) {
const d = await fetch('/sport/api/v2/akademski/list').then(r=>r.json());
let h = `<div class="card" style="padding:14px"><h3>🎓 Akademski sport (UNIRI) <span class="muted" style="font-size:11px">${d.count} klubova</span></h3>`;
h += '<table class="ri-tbl ri-sortable"><thead><tr><th>Klub</th><th>Fakultet</th><th>Sport</th><th>Razina</th><th>Članova</th><th>Web</th></tr></thead><tbody>';
for (const x of d.results) {
const safeKlub = (x.naziv||'').replace(/'/g,'');
const safeFak = (x.fakultet||'').replace(/'/g,'');
const safeSport = (x.sport||'').replace(/'/g,'');
h += `<tr>
<td style="cursor:pointer" onclick="state.searchQ='${safeKlub}';state.page='search';render()"><b style="color:var(--accent)">${x.naziv}</b></td>
<td class="muted" style="cursor:pointer" onclick="state.searchQ='${safeFak}';state.page='search';render()">${x.fakultet||'-'}</td>
<td style="cursor:pointer" onclick="state.searchQ='${safeSport} klubovi';state.page='search';render()">${x.sport}</td>
<td><span style="display:inline-block;padding:2px 6px;background:var(--bg4);border-radius:3px;font-size:10px">${x.razina||'-'}</span></td>
<td class="mono">${x.broj_clanova||'-'}</td>
<td>${x.web ? `<a href="${x.web}" target="_blank" class="muted">↗</a>` : ''}</td></tr>`;
}
h += '</tbody></table></div>';
box.innerHTML = h;
}
async function bazaVijesti(box) {
const d = await fetch('/sport/api/v2/vijesti/list?limit=50').then(r => r.json());
let html = `<div class="card" style="padding:14px"><h3 style="margin:0 0 10px 0">📰 Vijesti <span class="muted" style="font-size:11px">${d.count} vijesti</span></h3>`;
for (const v of d.results) {
html += `<div style="padding:10px;border-bottom:1px solid var(--border)">
<div style="display:flex;justify-content:space-between;align-items:start">
<div style="flex:1">
<b>${v.naslov||'?'}</b>
${v.kategorija ? `<span style="display:inline-block;padding:2px 6px;background:var(--bg4);border-radius:3px;font-size:10px;margin-left:8px">${v.kategorija}</span>` : ''}
${v.sazetak ? `<div class="muted" style="font-size:11px;margin-top:4px">${v.sazetak.slice(0,200)}…</div>` : ''}
</div>
<div class="muted" style="font-size:11px;margin-left:14px">${v.datum||''}</div>
</div>
${v.url ? `<a href="${v.url}" target="_blank" class="muted" style="font-size:10px">↗ ${v.url.slice(0,60)}</a>` : ''}
</div>`;
}
html += '</div>';
box.innerHTML = html;
}
// ═══════════════════════════════════════════════════════
// FUNKCIONARI PGŽ — IO/NO/Skupštinari saveza i klubova
// ═══════════════════════════════════════════════════════
async function pageFunkcionari() {
const c = document.getElementById('content');
setTopbar('Funkcionari PGŽ', 'Sportski rukovoditelji — saveza, klubova, skupština');
c.innerHTML = `
<div class="card" style="margin-bottom:14px;padding:14px">
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
<input id="fSearch" class="inp" placeholder="🔍 traži ime / prezime / funkcija / organizacija" style="flex:1;min-width:260px" oninput="fSearchType()">
<input id="fSportFilter" class="inp" placeholder="sport (npr. nogomet)" style="min-width:180px" oninput="fSearchType()">
<span class="muted" style="font-size:11px" id="fCount">—</span>
</div>
</div>
<div id="fGrid" class="card" style="padding:14px">Učitavam…</div>`;
await fLoad();
}
let _fT = null;
function fSearchType() { clearTimeout(_fT); _fT = setTimeout(fLoad, 300); }
async function fLoad() {
const q = (document.getElementById('fSearch')||{}).value || '';
const sp = (document.getElementById('fSportFilter')||{}).value || '';
const params = new URLSearchParams();
if (q) params.set('q', q);
if (sp) params.set('sport', sp);
params.set('limit', '300');
const grid = document.getElementById('fGrid');
try {
const d = await fetch('/sport/api/v2/osobe-funkcije/list?'+params.toString()).then(r=>r.json());
document.getElementById('fCount').textContent = `${d.count} funkcionara`;
if (!d.results || !d.results.length) {
grid.innerHTML = '<div class="muted" style="padding:40px;text-align:center">Nema rezultata.</div>';
return;
}
// Group by organizacija
const groups = {};
for (const r of d.results) {
const k = r.organizacija || r.savez_naziv || 'Ostali';
if (!groups[k]) groups[k] = [];
groups[k].push(r);
}
let html = '';
for (const [org, list] of Object.entries(groups)) {
html += `<div style="margin-bottom:18px">
<h3 style="margin-bottom:10px;color:var(--accent)">${org} <span class="muted" style="font-weight:400;font-size:11px">(${list.length})</span></h3>
<table class="ri-tbl ri-sortable"><thead><tr><th>#</th><th>Ime</th><th>Funkcija</th><th>Sport</th><th>Mandat</th><th>Izvor</th></tr></thead><tbody>`;
list.forEach((r,i) => {
const mandat = (r.mandate_od && r.mandate_do) ? `${r.mandate_od.slice(0,7)} → ${r.mandate_do.slice(0,7)}` : '—';
const url = r.izvor_url ? `<a href="${r.izvor_url}" target="_blank" class="muted" style="font-size:10px">${r.izvor||'link'}</a>` : (r.izvor||'—');
html += `<tr>
<td class="mono">${i+1}</td>
<td><b>${r.ime} ${r.prezime||''}</b></td>
<td>${r.funkcija||'—'}</td>
<td class="muted">${r.sport||''}</td>
<td class="mono" style="font-size:10px">${mandat}</td>
<td>${url}</td>
</tr>`;
});
html += '</tbody></table></div>';
}
grid.innerHTML = html;
} catch(e) { grid.innerHTML = '<div class="banner crit">Greška: '+e.message+'</div>'; }
}
// ═══════════════════════════════════════════════════════
// SPORTSKI STATS — Top Scorers, Top Appearances, Klub Breakdown
// ═══════════════════════════════════════════════════════
async function pageSportStats() {
const c = document.getElementById('content');
setTopbar('Sport Stats', 'Pregled svih sportova PGŽ');
window.navStack = []; // reset breadcrumb na entry stranici
c.innerHTML = '<div class="card" style="padding:24px">Učitavam statistiku...</div>';
try {
const d = await fetch('/sport/api/v2/sport/svi/stats').then(r=>r.json());
const t = d.totals || {};
const sportovi = (d.sportovi || []).filter(s => s.sport && s.sport !== 'općenito');
let html = '';
// Top KPI summary
html += `<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin-bottom:14px">
${[
['Sportova', sportovi.length, '🏆'],
['Klubova', t.klubova || 0, '🏟️'],
['Sportaša', t.sportasa || 0, '🏃'],
['Saveza', t.saveza || 0, '📋'],
].map(([lbl,val,ico]) => `
<div class="ri-card" style="padding:14px;text-align:center">
<div style="font-size:22px;margin-bottom:4px">${ico}</div>
<div class="ri-kpi-value">${(val||0).toLocaleString('hr-HR')}</div>
<div class="ri-kpi-label">${lbl}</div>
</div>`).join('')}
</div>`;
// Grid svih sportova - klikabilan
html += `<div class="card" style="margin-bottom:14px">
<h3 style="margin-bottom:12px">Sportovi PGŽ — kliknite za detalje</h3>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px">`;
sportovi.forEach(s => {
const ico = sportIcon(s.sport);
const isHns = s.sport === 'nogomet';
html += `<div class="ri-card" onclick="gotoSport('${s.sport.replace(/'/g, "&apos;")}')"
style="cursor:pointer;padding:12px;transition:transform 0.15s,border-color 0.15s"
onmouseover="this.style.transform='translateY(-2px)';this.style.borderColor='var(--accent)'"
onmouseout="this.style.transform='';this.style.borderColor=''">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px">
<div style="font-size:28px">${ico}</div>
<div style="flex:1">
<div style="font-weight:600;font-size:14px;color:var(--text-bright);text-transform:capitalize">${s.sport}</div>
${isHns ? '<div style="font-size:9px;color:var(--accent)">HNS Semafor sync</div>' : ''}
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;font-size:11px;color:var(--text2)">
<div><span class="muted">Klubova:</span> <b style="color:var(--text-bright)">${s.klubova||0}</b></div>
<div><span class="muted">Sportaša:</span> <b style="color:var(--text-bright)">${s.sportasa||0}</b></div>
<div><span class="muted">Saveza:</span> <b style="color:var(--text-bright)">${s.saveza||0}</b></div>
<div><span class="muted">Manif.:</span> <b style="color:var(--text-bright)">${s.manifestacija||0}</b></div>
${s.nagrada > 0 ? `<div style="grid-column:span 2"><span class="muted">Nagrada:</span> <b style="color:var(--amber)">${s.nagrada}</b></div>` : ''}
</div>
</div>`;
});
html += `</div></div>`;
c.innerHTML = html;
} catch(e) {
c.innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>';
}
}
function gotoSport(s) { state.page='sport'; state.sport_naziv = s; render(); }
async function pageSport() {
const sport = state.sport_naziv;
const c = document.getElementById('content');
setTopbar('Sport Stats', sport);
if (!sport) { c.innerHTML='<div class="banner crit">Nema sport.</div>'; return; }
c.innerHTML = '<div class="card" style="padding:24px">Učitavam ' + sport + '...</div>';
try {
const d = await fetch('/sport/api/v2/sport/' + encodeURIComponent(sport) + '/pregled').then(r=>r.json());
const ico = sportIcon(sport);
const st = d.stats || {};
let html = '';
// Breadcrumbs
html += breadcrumbs([
{label: '🏆 Sport Stats', onclick: "goto('sportStats')"},
{label: sportIcon(sport) + ' ' + sport.charAt(0).toUpperCase() + sport.slice(1)}
]);
// Header
html += `<div class="card" style="margin-bottom:14px;padding:18px;background:linear-gradient(135deg,var(--bg2),var(--bg3))">
<div style="display:flex;align-items:center;gap:18px">
<div style="font-size:54px">${ico}</div>
<div style="flex:1">
<h1 style="margin:0;text-transform:capitalize;color:var(--text-bright);font-size:26px">${sport}</h1>
<div class="muted" style="font-size:11px;margin-top:4px">Primorsko-goranska županija · ${st.broj_gradova||0} gradova</div>
</div>
<div onclick="goto('sportStats')" style="cursor:pointer;color:var(--accent);font-size:12px">← Svi sportovi</div>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:0;border-top:1px solid var(--border);margin-top:14px;padding-top:14px">
${[
['Klubova', st.broj_klubova, 'klubovi', null],
['Sportaša', st.broj_sportasa, 'sportasi', `M ${st.broj_sportasa_m||0} · Ž ${st.broj_sportasa_z||0}`],
['Kategoriziranih', st.broj_kategoriziranih, 'kategorizirani', `M ${st.broj_kategoriziranih_m||0} · Ž ${st.broj_kategoriziranih_z||0}`],
['Reprezentativaca', st.broj_reprezentativaca, 'reprezentativci', `M ${st.broj_reprezentativaca_m||0} · Ž ${st.broj_reprezentativaca_z||0}`],
['Saveza', d.savezi.length, 'savezi', null],
['Manifestacija', d.manifestacije.length, 'manifestacije', null],
].map(([lbl,v,filter,sub]) => `<div style="text-align:center;padding:6px;cursor:pointer;border-radius:6px;transition:background 0.2s" onmouseover="this.style.background='var(--bg3)'" onmouseout="this.style.background='transparent'" onclick="showSportasiModal('${sport}','${filter}','${lbl} u ${sport}')">
<div class="ri-kpi-value" style="font-size:20px">${(v||0).toLocaleString('hr-HR')}</div>
<div class="ri-kpi-label">${lbl}</div>
${sub ? `<div class="muted" style="font-size:10px;margin-top:2px">${sub}</div>` : ''}
</div>`).join('')}
</div>
</div>`;
// Saveze
if (d.savezi && d.savezi.length) {
html += `<div class="card" style="margin-bottom:14px"><h3 style="margin-bottom:10px">Savezi (${d.savezi.length})</h3>`;
d.savezi.forEach(s => {
html += `<div style="padding:8px 0;border-bottom:1px solid var(--border)">
<div style="font-weight:600">${s.naziv}</div>
<div class="muted" style="font-size:11px;margin-top:2px">
${s.predsjednik ? `Predsj.: ${s.predsjednik}` : ''}
${s.tajnik ? ` · Tajnik: ${s.tajnik}` : ''}
${s.godina_osnutka ? ` · od ${s.godina_osnutka}` : ''}
${s.grad ? ` · ${s.grad}` : ''}
</div>
</div>`;
});
html += `</div>`;
}
// Klubovi tabela
if (d.klubovi && d.klubovi.length) {
html += `<div class="card" style="margin-bottom:14px"><h3 style="margin-bottom:10px">Klubovi (${d.klubovi.length})</h3>
<table class="ri-tbl ri-sortable"><thead><tr><th>Klub</th><th>Razina</th><th>Grad</th><th>Osnovan</th>
<th style="text-align:right">Članova</th><th style="text-align:right">Kateg.</th></tr></thead><tbody>`;
d.klubovi.forEach(k => {
html += `<tr style="cursor:pointer" onclick="navPush();gotoKlubRoster(${k.id})" ondblclick="navPush();gotoKlubRoster(${k.id})" title="Klik za roster · Dvoklik za otvori">
<td><b>${k.naziv}</b>${k.hns_klub_id ? ' <span style="color:var(--accent);font-size:9px" title="Sinkroniziran s HNS Semafor COMET">HNS</span>' : ''}</td>
<td>${k.razina||'-'}</td>
<td>${k.grad||'-'}</td>
<td class="mono">${k.godina_osnutka||'-'}</td>
<td style="text-align:right">${k.broj_clanova||0}</td>
<td style="text-align:right;color:var(--accent)">${k.broj_kategoriziranih||0}</td>
</tr>`;
});
html += `</tbody></table></div>`;
}
// Top sportaši (kategorizirani / reprezentativci)
if (d.top_sportasi && d.top_sportasi.length) {
html += `<div class="card" style="margin-bottom:14px"><h3 style="margin-bottom:10px">Top osobe (kategorizirani sportaši, treneri, uprava) (${d.top_sportasi.length})</h3>
<table class="ri-tbl ri-sortable"><thead><tr><th>Foto</th><th>Ime i prezime</th><th>Uloga</th><th>Klub</th><th>Pozicija</th><th>HOO Kateg.</th><th>Repr.</th></tr></thead><tbody>`;
d.top_sportasi.forEach(s => {
html += `<tr style="cursor:pointer" onclick="navPush();gotoSportas(${s.id})" ondblclick="navPush();gotoSportas(${s.id})" title="Klik / Dvoklik za profil sportaša">
<td>${s.slika_url ? `<img src="${imgProxy(s.slika_url)}" style="width:32px;height:32px;border-radius:50%;object-fit:cover"/>` : '🏃'}</td>
<td><b>${s.ime||''} ${s.prezime||''}</b></td>
<td>${ulogaBadge(s.uloga)}</td>
<td>${s.klub_naziv||'-'}</td>
<td>${s.pozicija||'-'}</td>
<td><span class="risk-low">${s.kategorija_hoo ? 'I'.repeat(s.kategorija_hoo) : '-'}</span></td>
<td>${s.reprezentativac ? '<span class="risk-low">REPR</span>' : '-'}</td>
</tr>`;
});
html += `</tbody></table></div>`;
}
// Trofeji
if (d.trofeji && d.trofeji.length) {
html += `<div class="card" style="margin-bottom:14px"><h3 style="margin-bottom:10px">Povijesni trofeji (${d.trofeji.length})</h3>
<table class="ri-tbl ri-sortable"><thead><tr><th>Klub</th><th>Sezona</th><th>Natjecanje</th><th>Plasman</th><th>Trofej</th></tr></thead><tbody>`;
d.trofeji.forEach(t => {
html += `<tr ${t.klub_id?'style="cursor:pointer" onclick="gotoKlubRoster('+t.klub_id+')"':''}>
<td><b>${t.klub_naziv}</b></td>
<td class="mono">${t.sezona||'-'}</td>
<td>${t.natjecanje||'-'}</td>
<td style="text-align:center"><b style="color:${t.plasiranje==1?'gold':t.plasiranje==2?'silver':t.plasiranje==3?'#cd7f32':'var(--text2)'}">${t.plasiranje||'-'}.</b></td>
<td>${t.trofej||'-'}</td>
</tr>`;
});
html += `</tbody></table></div>`;
}
// Najbolji sportaši kroz godine
if (d.najbolji && d.najbolji.length) {
html += `<div class="card" style="margin-bottom:14px"><h3 style="margin-bottom:10px">Najbolji sportaši kroz godine (${d.najbolji.length})</h3>
<table class="ri-tbl ri-sortable"><thead><tr><th>Godina</th><th>Kategorija</th><th>Ime i prezime</th><th>Klub</th></tr></thead><tbody>`;
d.najbolji.forEach(n => {
html += `<tr>
<td class="mono"><b>${n.godina}</b></td>
<td>${n.kategorija||'-'}</td>
<td><b>${n.ime_prezime}</b></td>
<td class="muted">${n.klub||'-'}</td>
</tr>`;
});
html += `</tbody></table></div>`;
}
// Manifestacije
if (d.manifestacije && d.manifestacije.length) {
html += `<div class="card" style="margin-bottom:14px"><h3 style="margin-bottom:10px">Manifestacije (${d.manifestacije.length})</h3>
<table class="ri-tbl ri-sortable"><thead><tr><th>Naziv</th><th>Mjesto</th><th>Razina</th><th>Od godine</th><th>Učesnika</th><th>Organizator</th></tr></thead><tbody>`;
d.manifestacije.forEach(m => {
const safeNaz = (m.naziv||'').replace(/'/g,'');
const safeMjesto = (m.mjesto||'').replace(/'/g,'');
const safeOrg = (m.organizator||'').replace(/'/g,'');
const mapsQ = encodeURIComponent(safeMjesto + ' Hrvatska');
const searchURL = `https://www.google.com/search?q=${encodeURIComponent(safeNaz + ' ' + safeMjesto)}`;
html += `<tr>
<td><a href="${searchURL}" target="_blank" style="color:var(--accent);text-decoration:none"><b>${m.naziv}</b></a></td>
<td>${m.mjesto ? `<a href="https://www.google.com/maps/search/?api=1&query=${mapsQ}" target="_blank" class="muted">📍 ${m.mjesto}</a>` : '-'}</td>
<td>${m.razina||'-'}</td>
<td class="mono">${m.godina_od||'-'}</td>
<td>${m.broj_ucesnika||'-'}</td>
<td class="muted" style="font-size:11px;cursor:pointer" onclick="state.searchQ='${safeOrg}';state.page='search';render()">${m.organizator||'-'}</td>
</tr>`;
});
html += `</tbody></table></div>`;
}
if (!d.savezi.length && !d.klubovi.length && !d.top_sportasi.length) {
html += `<div class="card" style="padding:24px;text-align:center;color:var(--text3)">Nema podataka za sport "${sport}". Možda treba popuniti podatke.</div>`;
}
c.innerHTML = html;
} catch(e) {
c.innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>';
}
}
// (old pageSportStats removed)
async function spDoAdd() {
const promo_checks = document.querySelectorAll('input[name="spPromo"]:checked');
const promo_selected = Array.from(promo_checks).map(c => c.value);
const body = {
ime: document.getElementById('spIme').value.trim(),
prezime: document.getElementById('spPrezime').value.trim(),
klub_id: parseInt(document.getElementById('spKlub').value) || null,
datum_rodenja: document.getElementById('spDob').value || null,
mjesto_rodenja: document.getElementById('spMjesto').value.trim() || null,
broj_dresa: parseInt(document.getElementById('spDres').value) || null,
pozicija: document.getElementById('spPozicija').value || null,
dominantna_noga: document.getElementById('spNoga').value || null,
visina_cm: parseInt(document.getElementById('spVisina').value) || null,
tezina_kg: parseInt(document.getElementById('spTezina').value) || null,
slika_url: document.getElementById('spSlika').value.trim() || null,
oib: document.getElementById('spOib').value.trim() || null,
biografija: document.getElementById('spBio').value.trim() || null,
sport: (document.getElementById('spSport')||{}).value || null,
spol: (document.getElementById('spSpol')||{}).value || null,
promocija_kategorije: promo_selected,
};
const err = document.getElementById('spErr');
err.style.display = 'none';
if (!body.ime || !body.prezime || !body.klub_id) {
err.textContent = 'Ime, prezime i klub_id su obavezni';
err.style.display = 'block';
return;
}
try {
const d = await api('/api/v2/sportas/create', { method:'POST', body: JSON.stringify(body) });
alert('Kreiran sportaš #' + d.id);
usrModalClose();
if (typeof spLoad === 'function') spLoad();
if (typeof gotoSportas === 'function' && d.id) gotoSportas(d.id);
} catch(e) { err.textContent = e.message; err.style.display = 'block'; }
}
// ═══════════════════════════════════════════════════════
// SPORTAŠI GRID — pretraživa galerija svih sportaša
// ═══════════════════════════════════════════════════════
async function pageSportasi() {
const c = document.getElementById('content');
setTopbar('Sportaši', 'Pregled svih sportaša PGŽ');
c.innerHTML = `
<div class="card" style="margin-bottom:14px;padding:14px">
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
<input id="spSearch" class="inp" placeholder="🔍 traži po imenu, prezimenu" style="flex:1;min-width:240px" oninput="spSearchType()">
<select id="spKlubFilter" class="inp" style="min-width:200px" onchange="spLoad()">
<option value="">Svi klubovi</option>
</select>
<span class="muted" style="font-size:11px" id="spCount">—</span>
<button class="btn ri-btn-primary" onclick="spAddNew()"> Novi sportaš</button>
</div>
</div>
<div id="spGrid" class="card" style="padding:14px">Učitavam…</div>
`;
await spLoadKlubovi();
await spLoad();
}
let _spTimer = null;
function spSearchType() { clearTimeout(_spTimer); _spTimer = setTimeout(spLoad, 300); }
async function spLoadKlubovi() {
try {
const r = await fetch('/sport/api/klubovi');
const d = await r.json();
const sel = document.getElementById('spKlubFilter');
if (!sel) return;
// Ispravi: uzmi niz iz različitih mogućih polja
let klubovi = [];
if (Array.isArray(d)) klubovi = d;
else if (d.results && Array.isArray(d.results)) klubovi = d.results;
else if (d.klubovi && Array.isArray(d.klubovi)) klubovi = d.klubovi;
else {
console.warn('Neočekivani format odgovora:', d);
klubovi = [];
}
if (klubovi.length) {
klubovi.sort((a,b) => (a.naziv||'').localeCompare(b.naziv||''));
sel.innerHTML = '<option value="">Svi klubovi</option>' +
klubovi.map(k => `<option value="${k.id}">${k.naziv}</option>`).join('');
} else {
sel.innerHTML = '<option value="">Svi klubovi</option>';
}
} catch(e) { console.error('klubovi load', e); }
}
async function spLoad() {
const q = (document.getElementById('spSearch')||{}).value || '';
const kid = (document.getElementById('spKlubFilter')||{}).value || '';
const params = new URLSearchParams();
if (q) params.set('q', q);
if (kid) params.set('klub_id', kid);
params.set('limit', '60');
const grid = document.getElementById('spGrid');
if (!grid) return;
try {
const d = await fetch('/sport/api/v2/sportas/search?' + params.toString()).then(r=>r.json());
document.getElementById('spCount').textContent = `${d.count} sportaša`;
if (!d.results || d.results.length === 0) {
grid.innerHTML = '<div class="muted" style="padding:40px;text-align:center">Nema rezultata. Promijeni filter ili pretragu.</div>';
return;
}
let html = '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px">';
for (const s of d.results) {
const dob = s.datum_rodenja ? new Date(s.datum_rodenja) : null;
const age = dob ? Math.floor((new Date() - dob) / (365.25 * 86400000)) : null;
const photo = s.slika_url || '';
html += `<div class="ri-card" onclick="gotoSportas(${s.id})" style="cursor:pointer;text-align:center;padding:12px;transition:transform 0.15s">
<div style="width:80px;height:80px;border-radius:50%;overflow:hidden;background:var(--bg4);margin:0 auto 8px;border:2px solid var(--border2)">
${photo ? `<img src="${imgProxy(photo)}" style="width:100%;height:100%;object-fit:cover" loading="lazy"/>` : '<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:30px;color:var(--text3)">🏃</div>'}
</div>
<div style="font-size:12px;font-weight:600;color:var(--text-bright)">${s.ime||''} ${s.prezime||''}</div>
<div class="muted" style="font-size:10px;margin-top:2px">${s.klub||'—'}</div>
${age ? `<div class="muted" style="font-size:10px">${age} g.</div>` : ''}
</div>`;
}
html += '</div>';
grid.innerHTML = html;
} catch(e) {
grid.innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>';
}
}
// ═══════════════════════════════════════════════════════
// SPORTAŠ PROFIL — semafor.hns.family stil
// ═══════════════════════════════════════════════════════
function gotoSportas(id) { state.page='sportas'; state.sportas_id=id; render(); }
function gotoKlubRoster(id) { state.page='klubRoster'; state.klub_id=id; render(); }
async function pageSportas() {
const cid = state.sportas_id;
const c = document.getElementById("content");
setTopbar("Sportaši", "Profil sportaša #" + cid);
if (!cid) { c.innerHTML='<div class="banner crit">Nema ID sportaša.</div>'; return; }
c.innerHTML = '<div class="card" style="padding:24px">Učitavam profil…</div>';
try {
const d = await api('/api/v2/clanovi/'+cid+'/full-profile');
if (d.sezone && !d.seasons) d.seasons = d.sezone;
if (d.karijera && !d.career) d.career = d.karijera;
if (d.utakmice && !d.matches) d.matches = d.utakmice;
const sp = d.sportas;
const photo = sp.slika_url || '';
const dob = sp.datum_rodenja ? new Date(sp.datum_rodenja) : null;
const age = dob ? Math.floor((new Date() - dob) / (365.25 * 86400000)) : null;
let html = breadcrumbs([
{label: '🏃 Sportaši', onclick: "goto('sportasi')"},
sp.klub_naziv ? {label: '🏟️ ' + sp.klub_naziv, onclick: `navPush();gotoKlubRoster(${sp.klub_id})`} : null,
{label: (sp.ime || '?') + ' ' + (sp.prezime || '?')}
].filter(Boolean));
if (!sp.source_url || sp.source === 'manual') {
html += `<div class="banner" style="background:rgba(245,158,11,0.1);border:1px solid var(--amber);color:var(--amber);padding:8px 12px;margin-bottom:12px;border-radius:6px;font-size:12px">
⚠ Podaci o ovoj osobi su unijeti ručno i mogu biti nepotpuni. ${sp.source_url ? 'Izvor: <a href="'+sp.source_url+'" target="_blank">'+sp.source_url+'</a>' : 'Izvor nije naveden.'}
</div>`;
}
html += `<div class="card" style="padding:0;overflow:hidden;margin-bottom:14px">
<div style="display:grid;grid-template-columns:auto 1fr;gap:18px;padding:18px;background:linear-gradient(135deg,var(--bg2),var(--bg3))">
<div style="width:120px;height:120px;border-radius:50%;overflow:hidden;border:2px solid var(--border2);background:var(--bg4)">
${photo ? `<img src="${imgProxy(photo)}" style="width:100%;height:100%;object-fit:cover" alt="${sp.ime} ${sp.prezime}"/>` : '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text3);font-size:32px">🏃</div>'}
</div>
<div>
<h1 style="font-size:24px;font-weight:600;color:var(--text-bright);margin:0">${sp.ime||''} ${sp.prezime||''}</h1>
<div style="margin-top:6px">${ulogaBadge(sp.uloga)}</div>
<div style="display:flex;gap:18px;flex-wrap:wrap;margin-top:10px;font-size:12px;color:var(--text2)">
${sp.klub_naziv ? `<div onclick="navPush();gotoKlubRoster(${sp.klub_id})" style="cursor:pointer;color:var(--accent)" title="Klik za klub roster">${sportIcon(sp.sport)} ${sp.klub_naziv}</div>` : ''}
${sp.sport ? `<div>📍 ${sp.sport}</div>` : ''}
${sp.razina ? `<div class="mono" style="color:var(--text3)">${sp.razina}</div>` : ''}
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:14px;margin-top:18px">
<div><div class="ri-kpi-label">Datum rođenja</div><div style="font-size:13px;color:var(--text2)">
${dob ? dob.toLocaleDateString('hr-HR') + (age?' <span class="muted">('+age+' g)</span>':'') : (sp.godina_rodenja ? sp.godina_rodenja + ' <span class="muted">(samo godina)</span>' : '<span style="color:var(--text3);font-style:italic">— nepoznato —</span>')}
</div></div>
<div><div class="ri-kpi-label">Mjesto rođenja</div><div style="font-size:13px;color:var(--text2)">${sp.mjesto_rodenja || '<span style="color:var(--text3);font-style:italic">— nepoznato —</span>'}</div></div>
${sp.pozicija ? `<div><div class="ri-kpi-label">Pozicija</div><div style="font-size:13px;color:var(--text2)">${sp.pozicija}</div></div>`:''}
${sp.broj_dresa ? `<div><div class="ri-kpi-label">Broj dresa</div><div class="mono" style="font-size:18px;color:var(--accent);font-weight:700">${sp.broj_dresa}</div></div>`:''}
</div>
</div>
</div>`;
// Dinamički KPI ovisno o sportu
const sport = (sp.sport || "").toLowerCase();
const totals = d.totals || {};
let kpis = [];
if (sport === "nogomet") kpis = [["Nastupi", totals.nastupa||0], ["Pogoci", totals.pogodaka||0, "var(--accent)"], ["Žuti", totals.zutih||0, "var(--amber)"], ["Crveni", totals.crvenih||0, "var(--red)"]];
else if (sport === "rukomet") kpis = [["Nastupi", totals.nastupa||0], ["Golovi", totals.pogodaka||0, "var(--accent)"], ["Asistencije", totals.asistencije||0], ["Isključenja", totals.iskljucenja||0, "var(--amber)"]];
else if (sport === "košarka" || sport === "kosarka") kpis = [["Nastupi", totals.nastupa||0], ["Poeni", totals.poeni||0, "var(--accent)"], ["Asistencije", totals.asistencije||0], ["Skokovi", totals.skokovi||0], ["Faulovi", totals.faulovi||0, "var(--amber)"]];
else if (sport === "odbojka") kpis = [["Nastupi", totals.nastupa||0], ["Poeni", totals.poeni||0, "var(--accent)"], ["Blokovi", totals.blokovi||0], ["Servisi", totals.servisi||0]];
else if (sport === "vaterpolo") kpis = [["Nastupi", totals.nastupa||0], ["Golovi", totals.pogodaka||0, "var(--accent)"], ["Isključenja", totals.iskljucenja||0, "var(--amber)"], ["Blokovi", totals.blokovi||0]];
else kpis = [["Nastupi", totals.nastupa||0], ["Ukupno", totals.pogodaka||0, "var(--accent)"]];
html += `<div style="display:grid;grid-template-columns:repeat(${kpis.length},1fr);gap:0;border-top:1px solid var(--border)">`;
kpis.forEach(k => { html += `<div style="padding:12px;text-align:center;border-right:1px solid var(--border)"><div class="ri-kpi-value" style="${k[2]?`color:${k[2]}`:""}">${k[1]}</div><div class="ri-kpi-label">${k[0]}</div></div>`; });
html += `</div></div>`;
if (d.seasons && d.seasons.length) {
html += `<div class="card" style="margin-bottom:14px"><h3 style="margin-bottom:10px">Statistika po sezonama</h3><table class="ri-tbl ri-sortable"><thead><tr>`;
if (sport === "nogomet") html += `<th>Sezona</th><th>Natjecanje</th><th style="text-align:right">Nastupi</th><th style="text-align:right">Pogoci</th><th style="text-align:right">Žuti</th><th style="text-align:right">Crveni</th><th style="text-align:right">Minute</th>`;
else if (sport === "rukomet") html += `<th>Sezona</th><th>Natjecanje</th><th style="text-align:right">Nastupi</th><th style="text-align:right">Golovi</th><th style="text-align:right">Asistencije</th><th style="text-align:right">Isključenja</th>`;
else if (sport === "košarka" || sport === "kosarka") html += `<th>Sezona</th><th>Natjecanje</th><th style="text-align:right">Nastupi</th><th style="text-align:right">Poeni</th><th style="text-align:right">Asistencije</th><th style="text-align:right">Skokovi</th><th style="text-align:right">Faulovi</th>`;
else if (sport === "odbojka") html += `<th>Sezona</th><th>Natjecanje</th><th style="text-align:right">Nastupi</th><th style="text-align:right">Poeni</th><th style="text-align:right">Blokovi</th><th style="text-align:right">Servisi</th>`;
else html += `<th>Sezona</th><th>Natjecanje</th><th style="text-align:right">Nastupi</th><th style="text-align:right">Ukupno</th>`;
html += `</tr></thead><tbody>`;
d.seasons.forEach(r => {
html += `<tr><td>${r.sezona||''}</td><td>${r.natjecanje||''}</td><td style="text-align:right">${r.nastupi}</td>`;
if (sport === "nogomet") html += `<td style="text-align:right;color:var(--accent)">${r.pogoci}</td><td style="text-align:right">${r.zuti}</td><td style="text-align:right">${r.crveni}</td><td style="text-align:right" class="mono">${r.minute_total||'-'}</td>`;
else if (sport === "rukomet") html += `<td style="text-align:right;color:var(--accent)">${r.pogoci}</td><td style="text-align:right">${r.asistencije||0}</td><td style="text-align:right">${r.iskljucenja||0}</td>`;
else if (sport === "košarka" || sport === "kosarka") html += `<td style="text-align:right;color:var(--accent)">${r.poeni||0}</td><td style="text-align:right">${r.asistencije||0}</td><td style="text-align:right">${r.skokovi||0}</td><td style="text-align:right">${r.faulovi||0}</td>`;
else if (sport === "odbojka") html += `<td style="text-align:right;color:var(--accent)">${r.poeni||0}</td><td style="text-align:right">${r.blokovi||0}</td><td style="text-align:right">${r.servisi||0}</td>`;
else html += `<td style="text-align:right;color:var(--accent)">${r.pogoci||0}</td>`;
html += `</tr>`;
});
html += `</tbody></table></div>`;
} else {
html += `<div class="card" style="margin-bottom:14px;padding:24px;text-align:center;color:var(--text3)">Nema zabilježenih utakmica. Podaci se osvježavaju iz HNS Semafor sustava (sezonski).</div>`;
}
if (d.career && d.career.length) {
html += `<div class="card" style="margin-bottom:14px"><h3 style="margin-bottom:10px">Karijera (klubovi)</h3><table class="ri-tbl ri-sortable"><thead><tr><th>Klub</th><th>Od</th><th>Do</th><th style="text-align:right">Nastupa</th></tr></thead><tbody>`;
d.career.forEach(r => {
html += `<tr style="cursor:pointer" onclick="navPush();gotoKlubRoster(${r.id})" title="Klik za klub"><td>${r.naziv}</td><td>${r.od_dat||'-'}</td><td>${r.do_dat||'-'}</td><td style="text-align:right">${r.nastupa}</td></tr>`;
});
html += `</tbody></table></div>`;
}
if (d.matches && d.matches.length) {
if (sport === "nogomet") {
html += `<div class="card"><h3 style="margin-bottom:10px">Posljednje utakmice (${d.matches.length})</h3><table class="ri-tbl ri-sortable"><thead><tr><th>Datum</th><th>Domaćin</th><th>:</th><th>Gost</th><th>Natjecanje</th><th style="text-align:right">Pogodaka</th><th>Kartoni</th><th style="text-align:right">Min</th></tr></thead><tbody>`;
d.matches.forEach(r => {
html += `<tr><td>${r.datum||'-'}</td><td>${r.klub_dom||'-'}</td><td class="mono" style="text-align:center;color:var(--text-bright)">${r.rezultat||''}</td><td>${r.klub_gost||'-'}</td><td>${r.natjecanje||''}</td><td style="text-align:right;color:var(--accent)">${r.pogodaka||0}</td><td><span class="risk-high">${r.zuti_kartoni||0}</span> <span class="risk-critical">${r.crveni_kartoni||0}</span></td><td style="text-align:right" class="mono">${r.minute||'-'}</td></tr>`;
});
} else {
html += `<div class="card"><h3 style="margin-bottom:10px">Posljednje utakmice (${d.matches.length})</h3><table class="ri-tbl ri-sortable"><thead><tr><th>Datum</th><th>Domaćin</th><th>:</th><th>Gost</th><th>Natjecanje</th></tr></thead><tbody>`;
d.matches.forEach(r => {
html += `<tr><td>${r.datum||'-'}</td><td>${r.klub_dom||'-'}</td><td class="mono">${r.rezultat||''}</td><td>${r.klub_gost||'-'}</td><td>${r.natjecanje||''}</td></tr>`;
});
}
html += `</tbody></table></div>`;
}
// A5_GUI_NAGRADE: HOO kategorija badge + medalje + povijesne nagrade
if (sp.kategorija_hoo) {
const kat_lbl = ['','I','II','III','IV','V','VI'][sp.kategorija_hoo] || sp.kategorija_hoo;
const kat_color = sp.kategorija_hoo===1?'var(--accent)':sp.kategorija_hoo===2?'#22c55e':sp.kategorija_hoo<=3?'#3b82f6':'var(--text2)';
// Inject HOO badge into header card (find first div after avatar, append)
// Easier: prepend a banner before everything below
html = html.replace(
'${sp.razina ? `<div class="mono"',
`<div style="background:${kat_color};color:#fff;padding:3px 10px;border-radius:12px;font-size:11px;font-weight:600">🏅 HOO KAT ${kat_lbl}</div>` +
'${sp.razina ? `<div class="mono"'
);
}
// Render nagrade (pojedinačne medalje 2025)
if (d.nagrade && d.nagrade.length) {
const sp_med = d.nagrade.filter(n => ['SP','EP','OI','SK'].includes(n.razina_natjecanja));
const dp_med = d.nagrade.filter(n => !['SP','EP','OI','SK'].includes(n.razina_natjecanja));
if (sp_med.length) {
html += `<div class="card" style="margin-bottom:14px;border-left:3px solid var(--accent)">
<h3 style="margin-bottom:10px">🏆 Međunarodne medalje (${sp_med.length})</h3>
<table class="ri-tbl ri-sortable"><thead><tr><th>Razina</th><th>Plasman</th><th>Natjecanje</th><th>Disciplina</th><th>Kategorija</th><th>Godina</th></tr></thead><tbody>`;
sp_med.forEach(n => {
const med_color = n.medalja==='ZLATO'?'#fbbf24':n.medalja==='SREBRO'?'#cbd5e1':n.medalja==='BRONCA'?'#d97706':'var(--text2)';
const med_emoji = n.medalja==='ZLATO'?'🥇':n.medalja==='SREBRO'?'🥈':n.medalja==='BRONCA'?'🥉':'';
html += `<tr><td><span class="mono" style="background:${med_color};color:#000;padding:2px 6px;border-radius:4px;font-size:10px;font-weight:700">${n.razina_natjecanja}</span></td>`;
html += `<td style="font-size:13px;color:${med_color};font-weight:600">${med_emoji} ${n.medalja||(n.plasman?n.plasman+'.':'-')}</td>`;
html += `<td>${n.natjecanje||'-'}</td><td>${n.disciplina||'-'}</td><td>${n.dobna_kategorija||'-'}</td><td class="mono">${n.godina||'-'}</td></tr>`;
});
html += `</tbody></table></div>`;
}
if (dp_med.length) {
html += `<div class="card" style="margin-bottom:14px">
<h3 style="margin-bottom:10px">🏅 Državne medalje (${dp_med.length})</h3>
<table class="ri-tbl ri-sortable"><thead><tr><th>Razina</th><th>Plasman</th><th>Natjecanje</th><th>Kategorija</th><th>Godina</th></tr></thead><tbody>`;
dp_med.forEach(n => {
const med_emoji = n.medalja==='ZLATO'?'🥇':n.medalja==='SREBRO'?'🥈':n.medalja==='BRONCA'?'🥉':'';
html += `<tr><td><span class="mono">${n.razina_natjecanja||'-'}</span></td>`;
html += `<td>${med_emoji} ${n.medalja||(n.plasman?n.plasman+'.':'-')}</td>`;
html += `<td>${n.natjecanje||'-'}</td><td>${n.dobna_kategorija||'-'}</td><td class="mono">${n.godina||'-'}</td></tr>`;
});
html += `</tbody></table></div>`;
}
}
// Render povijesne nagrade (najbolji_sportasi)
if (d.priznanja && d.priznanja.length) {
html += `<div class="card" style="margin-bottom:14px">
<h3 style="margin-bottom:10px">⭐ Priznanja po godinama (${d.priznanja.length})</h3>
<table class="ri-tbl ri-sortable"><thead><tr><th>Godina</th><th>Kategorija</th><th>Klub</th><th>Sport</th></tr></thead><tbody>`;
d.priznanja.forEach(p => {
html += `<tr><td class="mono"><b>${p.godina}</b></td><td>${p.kategorija||'-'}</td><td>${p.klub||'-'}</td><td>${p.sport||'-'}</td></tr>`;
});
html += `</tbody></table></div>`;
}
// Render klub_trofeji (klubske sezone)
if (d.klub_trofeji && d.klub_trofeji.length) {
html += `<div class="card" style="margin-bottom:14px">
<h3 style="margin-bottom:10px">🏟️ Trofeji kluba (${d.klub_trofeji.length})</h3>
<table class="ri-tbl ri-sortable"><thead><tr><th>Sezona</th><th>Natjecanje</th><th>Plasman</th><th>Trofej</th></tr></thead><tbody>`;
d.klub_trofeji.forEach(t => {
html += `<tr><td class="mono">${t.sezona||'-'}</td><td>${t.natjecanje||'-'}</td><td>${t.plasiranje?t.plasiranje+'.':'-'}</td><td style="color:var(--accent)">${t.trofej||'-'}</td></tr>`;
});
html += `</tbody></table></div>`;
}
if (sp.source_url) {
html += `<div style="margin-top:14px;font-size:10px;color:var(--text3);text-align:right">Izvor: <a href="${sp.source_url}" target="_blank" style="color:var(--accent)">${sp.source}</a> · Sync: ${sp.source_synced_at?new Date(sp.source_synced_at).toLocaleString('hr-HR'):'-'}</div>`;
}
// GODISNJAK_HISTORY_GUI - load and append history
try {
const gh = await api('/api/v2/sportas/'+sid+'/godisnjak_history');
if (gh && gh.count > 0) {
let ghHtml = `<div class="card" style="margin-top:14px">
<h3>📚 Godišnjaci ZS PGŽ — ${gh.count} godina pojavljivanja</h3>`;
gh.history.forEach(h => {
const m = h.has_medal ? '🥇' : '';
const k = h.has_kategorija ? '📋' : '';
const kw = (h.keywords||[]).slice(0,4).join(', ');
const snip = (h.snippet||'').replace(/[<>]/g,c=>({'<':'&lt;','>':'&gt;'}[c])).substring(0,500);
ghHtml += `<details style="margin-bottom:8px;padding:10px;background:rgba(255,255,255,0.03);border-radius:6px;border-left:3px solid var(--accent)">
<summary style="cursor:pointer;font-weight:600">
<span style="color:var(--accent)">${h.godina}</span> ${m} ${k}
${kw ? '<span class="muted" style="font-size:11px;margin-left:8px">['+kw+']</span>' : ''}
${h.izvor_url ? '<a href="'+h.izvor_url+'" target="_blank" style="float:right;font-size:11px;color:var(--accent)">PDF ↗</a>' : ''}
</summary>
<pre style="white-space:pre-wrap;font-size:11px;color:#bbb;margin-top:8px;font-family:monospace">${snip}</pre>
</details>`;
});
ghHtml += `</div>`;
html += ghHtml;
}
} catch(e) { console.warn('history load failed:', e); }
c.innerHTML = html;
} catch(e) { c.innerHTML = '<div class="banner crit">Greška: '+e.message+'</div>'; }
}
async function pageKlubRoster() {
const kid = state.klub_id;
const c = document.getElementById('content');
setTopbar('Klubovi', 'Roster kluba');
if (!kid) { c.innerHTML='<div class="banner crit">Nema ID kluba.</div>'; return; }
c.innerHTML = '<div class="card" style="padding:24px">Učitavam roster…</div>';
try {
const d = await api('/api/v2/klub/'+kid+'/sportasi');
const k = d.klub;
// Breadcrumb
let html = breadcrumbs([
{label: '🏟️ Klubovi', onclick: "goto('klubovi')"},
{label: k.sport ? sportIcon(k.sport) + ' ' + k.sport : '⚽', onclick: k.sport ? `goto('sport');state.sport_naziv='${k.sport}';render()` : null},
{label: k.naziv}
]);
html += `<div class="card" style="margin-bottom:14px">
<div style="display:flex;align-items:center;gap:14px">
${k.logo_url ? `<img src="${k.logo_url}" style="width:64px;height:64px;border-radius:8px"/>` : '<div style="width:64px;height:64px;border-radius:8px;background:var(--bg3);display:flex;align-items:center;justify-content:center;font-size:24px">⚽</div>'}
<div style="flex:1">
<h2 style="margin:0">${k.naziv}</h2>
<div class="muted" style="font-size:11px;margin-top:4px">${k.sport||''} · ${k.razina||''} · ${d.total} sportaša${k.hns_klub_id?' · <a href="https://semafor.hns.family/klubovi/'+k.hns_klub_id+'/'+(k.hns_slug||'')+'/" target="_blank" style="color:var(--accent)">HNS Semafor ↗</a>':''}</div>
</div>
${(state.user?.tip==='klub_admin' || state.user?.tip==='super_admin' || state.user?.tip==='pgz_admin') ? '<button class="btn-primary" onclick="addSportasPrompt('+kid+')">+ Sportaš</button>' : ''}
</div>
</div>`;
if (d.sportasi && d.sportasi.length) {
html += '<div class="card"><table class="ri-tbl ri-sortable"><thead><tr><th>#</th><th>Foto</th><th>Ime i prezime</th><th>Uloga</th><th>Datum rođenja</th><th>Pozicija</th><th style="text-align:right">Nast.</th><th style="text-align:right">Gol.</th><th>Izvor</th></tr></thead><tbody>';
d.sportasi.forEach(s => {
const dob = s.datum_rodenja ? new Date(s.datum_rodenja).toLocaleDateString('hr-HR') : '-';
html += `<tr style="cursor:pointer" onclick="navPush();gotoSportas(${s.id})" ondblclick="navPush();gotoSportas(${s.id})" title="Klik / Dvoklik za profil sportaša">
<td class="mono">${s.broj_dresa||'-'}</td>
<td>${s.slika_url ? `<img src="${imgProxy(s.slika_url)}" style="width:32px;height:32px;border-radius:50%;object-fit:cover"/>` : '🏃'}</td>
<td><b>${s.ime||''} ${s.prezime||''}</b> ${s.reprezentativac?'<span class="risk-low">REPR</span>':''}</td>
<td>${ulogaBadge(s.uloga)}</td>
<td>${dob}</td>
<td>${s.pozicija||'-'}</td>
<td style="text-align:right">${s.nastupa||0}</td>
<td style="text-align:right;color:var(--accent)">${s.pogoci||0}</td>
<td><span class="muted" style="font-size:10px">${s.source||'manual'}</span></td>
</tr>`;
});
html += '</tbody></table></div>';
} else {
html += '<div class="card" style="padding:24px;text-align:center;color:var(--text3)">Nema upisanih sportaša. Pokreni <code>hns_semafor.py</code> scraper ili dodaj ručno.</div>';
}
// A6_KLUBROSTER_PATCH: HOO sportaši, top medalisti, trofeji, povijesna priznanja
if (d.hoo_sportasi && d.hoo_sportasi.length) {
html += `<div class="card" style="margin-bottom:14px;border-left:3px solid var(--accent)">
<h3 style="margin-bottom:10px">🏅 HOO kategorizirani sportaši (${d.hoo_sportasi.length})</h3>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px">`;
d.hoo_sportasi.forEach(h => {
const kat_lbl = ['','I','II','III','IV','V','VI'][h.kategorija_hoo] || h.kategorija_hoo;
const kat_color = h.kategorija_hoo===1?'var(--accent)':h.kategorija_hoo===2?'#22c55e':h.kategorija_hoo<=3?'#3b82f6':'var(--text2)';
html += `<div style="padding:8px 10px;background:var(--bg3);border-radius:6px;cursor:pointer;display:flex;justify-content:space-between;align-items:center" onclick="navPush();gotoSportas(${h.id})">
<div><b>${h.ime||''} ${h.prezime||''}</b><div class="muted" style="font-size:10px">${h.sport||''}</div></div>
<span style="background:${kat_color};color:#fff;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:700">KAT ${kat_lbl}</span>
</div>`;
});
html += `</div></div>`;
}
if (d.top_medalisti && d.top_medalisti.length) {
html += `<div class="card" style="margin-bottom:14px">
<h3 style="margin-bottom:10px">🥇 Najuspješniji sportaši kluba (${d.top_medalisti.length})</h3>
<table class="ri-tbl ri-sortable"><thead><tr><th>Sportaš</th><th style="text-align:right">🥇</th><th style="text-align:right">🥈</th><th style="text-align:right">🥉</th><th style="text-align:right">SP/EP/OI</th><th style="text-align:right">Ukupno</th></tr></thead><tbody>`;
d.top_medalisti.forEach(m => {
const click = m.clan_id ? `style="cursor:pointer" onclick="navPush();gotoSportas(${m.clan_id})"` : '';
html += `<tr ${click}><td><b>${m.ime_prezime||'?'}</b></td>
<td style="text-align:right;color:#fbbf24">${m.z||0}</td>
<td style="text-align:right;color:#cbd5e1">${m.s||0}</td>
<td style="text-align:right;color:#d97706">${m.b||0}</td>
<td style="text-align:right;color:var(--accent);font-weight:600">${m.svj||0}</td>
<td style="text-align:right;color:var(--text-bright);font-weight:600">${m.nagrade}</td>
</tr>`;
});
html += `</tbody></table></div>`;
}
if (d.trofeji && d.trofeji.length) {
html += `<div class="card" style="margin-bottom:14px">
<h3 style="margin-bottom:10px">🏆 Trofeji i sezonski plasmani (${d.trofeji.length})</h3>
<table class="ri-tbl ri-sortable"><thead><tr><th>Sezona</th><th>Natjecanje</th><th style="text-align:right">Plasman</th><th>Trofej</th><th>Napomena</th></tr></thead><tbody>`;
d.trofeji.forEach(t => {
const plac_emoji = t.plasiranje===1?'🥇':t.plasiranje===2?'🥈':t.plasiranje===3?'🥉':'';
html += `<tr><td class="mono">${t.sezona||'-'}</td>
<td>${t.natjecanje||'-'}</td>
<td style="text-align:right">${plac_emoji}${t.plasiranje||''}</td>
<td style="color:var(--accent);font-weight:600">${t.trofej||'-'}</td>
<td class="muted" style="font-size:11px">${t.napomena ? t.napomena.substring(0,80) : ''}</td>
</tr>`;
});
html += `</tbody></table></div>`;
}
if (d.priznanja && d.priznanja.length) {
html += `<div class="card" style="margin-bottom:14px">
<h3 style="margin-bottom:10px">⭐ Povijesna priznanja sportaša kluba (${d.priznanja.length})</h3>
<table class="ri-tbl ri-sortable"><thead><tr><th>Godina</th><th>Sportaš</th><th>Kategorija</th></tr></thead><tbody>`;
d.priznanja.forEach(p => {
const click = p.clan_id ? `style="cursor:pointer" onclick="navPush();gotoSportas(${p.clan_id})"` : '';
html += `<tr ${click}><td class="mono"><b>${p.godina}</b></td><td>${p.ime_prezime||'-'}</td><td>${p.kategorija||'-'}</td></tr>`;
});
html += `</tbody></table></div>`;
}
// C6_NATJECANJA_PATCH: liga natjecanja u kojima sudjeluje klub
try {
const dn = await api('/api/v2/klub/'+kid+'/natjecanja');
if (dn.natjecanja && dn.natjecanja.length) {
html += `<div class="card" style="margin-bottom:14px;border-left:3px solid var(--accent)">
<h3 style="margin-bottom:10px">⚽ Liga natjecanja (${dn.natjecanja.length})</h3>
<table class="ri-tbl ri-sortable"><thead><tr><th>Liga</th><th>Sezona</th><th>Razina</th><th style="text-align:right">Pozicija</th><th style="text-align:right">Odigrano</th><th style="text-align:right">P-N-Por</th><th style="text-align:right">Bodovi</th></tr></thead><tbody>`;
dn.natjecanja.forEach(n => {
const place_emoji = n.pozicija===1?'🥇':n.pozicija===2?'🥈':n.pozicija===3?'🥉':'';
html += `<tr style="cursor:pointer" onclick="goto('natjecanjaTablica');state.natj_id=${n.id};render()">
<td><b>${n.naziv||'-'}</b></td>
<td class="mono">${n.sezona||'-'}</td>
<td>${n.razina||'-'}</td>
<td style="text-align:right">${place_emoji}${n.pozicija||'-'}</td>
<td style="text-align:right">${n.odigrano||0}</td>
<td style="text-align:right;font-size:11px">${n.pobjede||0}-${n.nerijeseno||0}-${n.porazi||0}</td>
<td style="text-align:right;color:var(--accent);font-weight:600">${n.bodovi||0}</td>
</tr>`;
});
html += `</tbody></table></div>`;
}
} catch(_) {}
c.innerHTML = html;
} catch(e) { c.innerHTML = '<div class="banner crit">Greška: '+e.message+'</div>'; }
}
// C6_NATJECANJA_PATCH: pages
async function pageNatjecanja() {
const c = document.getElementById('content');
setTopbar('Natjecanja', 'Liga tablice');
c.innerHTML = '<div class="card" style="padding:24px">Učitavam natjecanja…</div>';
try {
const d = await api('/api/v2/natjecanja?pgz_only=true&limit=50');
let html = `<div class="card"><h2>⚽ Liga natjecanja (PGŽ relevant)</h2>
<p class="muted">${d.count} aktivnih liga / natjecanja sa PGŽ klubovima</p></div>`;
// Group by sport
const by_sport = {};
d.natjecanja.forEach(n => {
if (!by_sport[n.sport]) by_sport[n.sport] = [];
by_sport[n.sport].push(n);
});
for (const [sport, lige] of Object.entries(by_sport)) {
html += `<div class="card" style="margin-bottom:14px">
<h3 style="margin-bottom:10px">${sportIcon(sport)} ${sport} (${lige.length})</h3>
<table class="ri-tbl ri-sortable"><thead><tr><th>Liga</th><th>Sezona</th><th>Razina</th><th>Klubovi</th><th>Savez</th></tr></thead><tbody>`;
lige.forEach(n => {
html += `<tr style="cursor:pointer" onclick="state.natj_id=${n.id};goto('natjecanjaTablica')">
<td><b>${n.naziv}</b></td>
<td class="mono">${n.sezona||'-'}</td>
<td>${n.razina||'-'}</td>
<td style="text-align:right">${n.broj_klubova}</td>
<td class="muted">${n.savez_naziv||'-'}</td>
</tr>`;
});
html += `</tbody></table></div>`;
}
c.innerHTML = html;
} catch(e) { c.innerHTML = '<div class="banner crit">Greška: '+e.message+'</div>'; }
}
async function pageNatjecanjaTablica() {
const nid = state.natj_id;
const c = document.getElementById('content');
setTopbar('Natjecanja', 'Liga tablica');
if (!nid) { c.innerHTML='<div class="banner crit">Nema ID natjecanja.</div>'; return; }
c.innerHTML = '<div class="card" style="padding:24px">Učitavam tablicu…</div>';
try {
const d = await api('/api/v2/natjecanja/'+nid+'/tablica');
const n = d.natjecanje;
let html = breadcrumbs([
{label: '⚽ Natjecanja', onclick: "goto('natjecanja')"},
{label: n.naziv}
]);
html += `<div class="card" style="margin-bottom:14px">
<h2>${sportIcon(n.sport)} ${n.naziv}</h2>
<div class="muted" style="font-size:11px">${n.razina||''} · sezona ${n.sezona||''} · ${d.broj_klubova} klubova · <a href="${n.source_url||'#'}" target="_blank" style="color:var(--accent)">izvor ↗</a></div>
</div>`;
html += `<div class="card"><table class="ri-tbl ri-sortable">
<thead><tr><th>Poz</th><th>Klub</th><th style="text-align:right">Od</th><th style="text-align:right">P</th><th style="text-align:right">N</th><th style="text-align:right">Por</th><th style="text-align:right">+</th><th style="text-align:right"></th><th style="text-align:right">+/</th><th style="text-align:right">Bod</th></tr></thead>
<tbody>`;
d.tablica.forEach(t => {
const place_emoji = t.pozicija===1?'🥇':t.pozicija===2?'🥈':t.pozicija===3?'🥉':'';
const klub_link = t.klub_id ? `style="cursor:pointer" onclick="navPush();state.klub_id=${t.klub_id};goto('klubRoster')"` : '';
const klub_name = t.klub_naziv_db || t.klub_naziv;
const logo = t.logo_url ? `<img src="${t.logo_url}" style="width:18px;height:18px;border-radius:3px;vertical-align:middle;margin-right:6px"/>` : '';
html += `<tr ${klub_link}>
<td class="mono"><b>${place_emoji}${t.pozicija}</b></td>
<td>${logo}<b>${klub_name}</b>${t.klub_id?' <span class="muted" style="font-size:10px">↗</span>':''}</td>
<td style="text-align:right">${t.odigrano||0}</td>
<td style="text-align:right;color:#22c55e">${t.pobjede||0}</td>
<td style="text-align:right">${t.nerijeseno||0}</td>
<td style="text-align:right;color:#ef4444">${t.porazi||0}</td>
<td style="text-align:right">${t.gol_z||0}</td>
<td style="text-align:right">${t.gol_p||0}</td>
<td style="text-align:right">${(t.gol_razlika||0) > 0 ? '+' : ''}${t.gol_razlika||0}</td>
<td style="text-align:right;color:var(--accent);font-weight:700">${t.bodovi||0}</td>
</tr>`;
});
html += `</tbody></table></div>`;
c.innerHTML = html;
} catch(e) { c.innerHTML = '<div class="banner crit">Greška: '+e.message+'</div>'; }
}
async function addSportasPrompt(klub_id) {
const ime = prompt('Ime sportaša:'); if (!ime) return;
const prezime = prompt('Prezime sportaša:'); if (!prezime) return;
const broj = prompt('Broj dresa (opcionalno):') || null;
const pozicija = prompt('Pozicija (npr. Vratar / Igrač / Centarfor):') || null;
try {
const d = await api('/api/v2/sportas/create', { method:'POST', body: JSON.stringify({
klub_id, ime, prezime, broj_dresa: broj?parseInt(broj):null, pozicija
})});
alert('✓ Dodan sportaš ID '+d.id);
pageKlubRoster();
} catch(e) { alert('Greška: '+e.message); }
}
async function pageGodisnjaci() {
// DOKUMENTI_GUI_PATCH
const c = document.getElementById('content');
setTopbar('Dokumenti', 'Godišnjaci + savez novosti');
c.innerHTML = '<div class="card" style="padding:24px">Učitavam dokumente…</div>';
try {
const [godisnjaci, novosti] = await Promise.all([
api('/api/v2/dokumenti?vrsta=godisnjak&limit=30'),
api('/api/v2/dokumenti?vrsta=novost_savez&limit=30')
]);
let html = `<div class="card" style="margin-bottom:14px">
<h2>📚 Dokumenti & izvori</h2>
<p class="muted" style="font-size:11px">${godisnjaci.count} godišnjaka + ${novosti.count} savez novosti</p>
<div style="margin-top:12px;display:flex;gap:8px">
<input id="dokSearch" placeholder="Pretraga kroz 18 godišnjaka (2006-2024)…"
style="flex:1;padding:10px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);color:#fff;border-radius:6px"/>
<button onclick="searchDokumenti()" style="padding:10px 20px;background:var(--accent);color:#000;border:0;border-radius:6px;cursor:pointer">🔍 Traži</button>
</div>
<div style="display:flex;gap:6px;margin-top:8px;flex-wrap:wrap;font-size:11px">
<span class="muted">Brzi pretrag:</span>
<a onclick="state.dokQuery='Mirza Džomba';searchDokumenti()" style="color:var(--accent);cursor:pointer">Mirza Džomba</a>
<a onclick="state.dokQuery='HNK Rijeka';searchDokumenti()" style="color:var(--accent);cursor:pointer">HNK Rijeka</a>
<a onclick="state.dokQuery='Petar Klovar';searchDokumenti()" style="color:var(--accent);cursor:pointer">Petar Klovar</a>
<a onclick="state.dokQuery='Vitomir Maričić';searchDokumenti()" style="color:var(--accent);cursor:pointer">Vitomir Maričić</a>
<a onclick="state.dokQuery='Sara Kolak';searchDokumenti()" style="color:var(--accent);cursor:pointer">Sara Kolak</a>
<a onclick="state.dokQuery='RK Zamet';searchDokumenti()" style="color:var(--accent);cursor:pointer">RK Zamet</a>
<a onclick="state.dokQuery='najuspješniji';searchDokumenti()" style="color:var(--accent);cursor:pointer">najuspješniji</a>
</div>
<div id="dokSearchResults" style="margin-top:12px"></div>
</div>`;
// Godišnjaci grid
html += `<div class="card" style="margin-bottom:14px">
<h3 style="margin-bottom:10px">📖 Sportski godišnjaci ZS PGŽ (${godisnjaci.count})</h3>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px">`;
godisnjaci.dokumenti.forEach(d => {
const chars = d.chars ? `${(d.chars/1000).toFixed(0)}k chars` : '';
html += `<div class="card" style="cursor:pointer;background:rgba(245,158,11,0.05);border-left:3px solid var(--accent);padding:14px"
onclick="state.dok_id=${d.id};goto('dokumentDetail')">
<div style="font-size:24px;font-weight:700;color:var(--accent)">${d.godina||'?'}</div>
<div style="font-size:11px;margin-top:4px">${d.organizacija||'-'}</div>
<div class="mono" style="font-size:10px;color:#aaa;margin-top:8px">${chars}</div>
</div>`;
});
html += `</div></div>`;
// Savez novosti
if (novosti.count > 0) {
html += `<div class="card" style="margin-bottom:14px">
<h3 style="margin-bottom:10px">📰 Savez novosti (PGŽ-relevant) (${novosti.count})</h3>
<table class="ri-tbl ri-sortable"><thead><tr><th>Datum</th><th>Naslov</th><th>Savez</th></tr></thead><tbody>`;
novosti.dokumenti.forEach(d => {
const dt = d.izdano_datum ? new Date(d.izdano_datum).toLocaleDateString('hr-HR') : '-';
html += `<tr style="cursor:pointer" onclick="window.open('${d.izvor_url}','_blank')">
<td class="mono" style="font-size:11px">${dt}</td>
<td><b>${d.title}</b></td>
<td class="muted" style="font-size:11px">${d.organizacija||'-'}</td>
</tr>`;
});
html += `</tbody></table></div>`;
}
c.innerHTML = html;
} catch(e) { c.innerHTML = '<div class="banner crit">Greška: '+e.message+'</div>'; }
}
window.searchDokumenti = async function() {
const q = state.dokQuery || (document.getElementById('dokSearch') ? document.getElementById('dokSearch').value : '');
if (!q || q.length < 2) return;
const target = document.getElementById('dokSearchResults');
if (!target) return;
target.innerHTML = '<p class="muted">Pretraga…</p>';
try {
const d = await api('/api/v2/dokumenti/search/q?q='+encodeURIComponent(q));
let html = `<h4 style="margin:12px 0 8px">📍 Rezultati za "${q}" (${d.count})</h4>`;
if (d.count === 0) {
html += `<p class="muted">Nema rezultata.</p>`;
} else {
html += `<div style="max-height:400px;overflow-y:auto">`;
d.rezultati.forEach(r => {
const yr = r.godina ? `<span style="color:var(--accent);font-weight:700">${r.godina}</span>` : '';
html += `<div style="margin-bottom:8px;padding:10px;background:rgba(255,255,255,0.03);border-radius:6px">
${yr} <b>${r.title}</b><br/>
<div class="mono" style="font-size:11px;margin-top:4px;color:#bbb;white-space:pre-wrap">${(r.excerpt||'').substring(0,500)}</div>
${r.izvor_url ? '<a href="'+r.izvor_url+'" target="_blank" style="font-size:11px;color:var(--accent)">izvor ↗</a>' : ''}
</div>`;
});
html += `</div>`;
}
target.innerHTML = html;
state.dokQuery = '';
} catch(e) { target.innerHTML = '<p style="color:#ef4444">Greška: '+e.message+'</p>'; }
}
async function pageDokumentDetail() {
// DOKUMENT_DETAIL
const did = state.dok_id;
const c = document.getElementById('content');
setTopbar('Dokument', 'Detalji');
if (!did) { c.innerHTML='<div class="banner crit">Nema ID dokumenta.</div>'; return; }
c.innerHTML = '<div class="card" style="padding:24px">Učitavam…</div>';
try {
const d = await api('/api/v2/dokumenti/'+did);
let html = breadcrumbs([
{label: '📚 Dokumenti', onclick: "goto('godisnjaci')"},
{label: d.title}
]);
html += `<div class="card" style="margin-bottom:14px">
<h2>${d.title}</h2>
<p class="muted" style="font-size:11px">
${d.organizacija||''} · ${d.godina||''} · ${d.vrsta||''} ·
${d.sadrzaj?d.sadrzaj.length.toLocaleString('hr-HR'):0} znakova
${d.izvor_url?'· <a href="'+d.izvor_url+'" target="_blank" style="color:var(--accent)">izvor PDF ↗</a>':''}
</p>
</div>`;
if (d.kratak_opis) {
html += `<div class="card" style="margin-bottom:14px"><h3>Sažetak</h3><p>${d.kratak_opis}</p></div>`;
}
if (d.sadrzaj) {
html += `<div class="card" style="margin-bottom:14px">
<h3>Sadržaj (${(d.sadrzaj.length/1000).toFixed(0)}k chars)</h3>
<pre style="white-space:pre-wrap;background:rgba(0,0,0,0.3);padding:14px;border-radius:6px;font-size:11px;max-height:600px;overflow-y:auto;font-family:monospace">${d.sadrzaj.substring(0,30000).replace(/[<>&]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;'}[c]))}${d.sadrzaj.length>30000?'\n\n…[truncated, '+d.sadrzaj.length+' total chars]':''}</pre>
</div>`;
}
c.innerHTML = html;
} catch(e) { c.innerHTML = '<div class="banner crit">Greška: '+e.message+'</div>'; }
}
async function pageCoverage() {
// COVERAGE_MATRIX_GUI
const c = document.getElementById('content');
setTopbar('Pokrivenost klubova', 'Heat-map kvalitete podataka');
c.innerHTML = '<div class="card" style="padding:24px">Učitavam pokrivenost…</div>';
try {
const d = await api('/api/v2/audit/coverage_matrix?limit=80');
// Heat-map function: 0 = red, max = green
const heat = (v, max) => {
if (!v || v === 0) return 'rgba(239,68,68,0.15)';
const r = v / Math.max(1,max);
if (r < 0.2) return 'rgba(239,68,68,0.4)';
if (r < 0.5) return 'rgba(245,158,11,0.4)';
if (r < 0.8) return 'rgba(132,204,22,0.4)';
return 'rgba(34,197,94,0.5)';
};
// Calculate maxes
const maxes = {
sportasa: Math.max(...d.klubovi.map(k => k.sportasa || 0)),
utakmica: Math.max(...d.klubovi.map(k => k.utakmica || 0)),
trofeja: Math.max(...d.klubovi.map(k => k.trofeja || 0)),
nagrada: Math.max(...d.klubovi.map(k => k.nagrada || 0)),
u_ligama: Math.max(...d.klubovi.map(k => k.u_ligama || 0)),
godina_god: 18
};
let html = `<div class="card" style="margin-bottom:14px">
<h2>🗂️ Heat-map pokrivenosti kluba</h2>
<p class="muted" style="font-size:11px">${d.count} aktivnih klubova · sortirano po kompozitnom skoru (sportaši + utakmice/10 + godišnjak×5)</p>
<div style="display:flex;gap:14px;margin-top:8px;font-size:11px">
<span><b style="color:#22c55e">●</b> ≥80%</span>
<span><b style="color:#84cc16">●</b> 50-80%</span>
<span><b style="color:#f59e0b">●</b> 20-50%</span>
<span><b style="color:#ef4444">●</b> <20%</span>
</div>
</div>`;
html += '<div class="card" style="overflow-x:auto">';
html += '<table class="ri-tbl ri-sortable"><thead><tr>' +
'<th style="position:sticky;left:0;background:#262624">Klub</th>' +
'<th>Sport</th>' +
'<th>Sportaši</th>' +
'<th>Utakmica</th>' +
'<th>Sezona</th>' +
'<th>Trofeja</th>' +
'<th>Nagrada</th>' +
'<th>U ligama</th>' +
'<th>Logo</th>' +
'<th>Godišnjak</th>' +
'<th>Period</th>' +
'</tr></thead><tbody>';
d.klubovi.forEach(k => {
const cell = (v, max) => `<td style="text-align:center;background:${heat(v, max)}">${v||0}</td>`;
const period = k.godisnjak_prvi && k.godisnjak_zadnji ?
`<span class="mono" style="font-size:10px">${k.godisnjak_prvi}-${k.godisnjak_zadnji}</span>` : '-';
const logo = k.ima_logo ? '<span style="color:#22c55e">✓</span>' : '<span style="color:#ef4444">✗</span>';
html += `<tr style="cursor:pointer" onclick="state.klub_id=${k.id};goto('klubRoster')">` +
`<td style="position:sticky;left:0;background:#262624;font-weight:600"><b>${k.naziv}</b>` +
(k.hns_klub_id ? `<br/><span class="mono" style="font-size:10px;color:#888">HNS#${k.hns_klub_id}</span>` : '') +
`</td>` +
`<td><span class="muted" style="font-size:11px">${k.sport||'-'}</span></td>` +
cell(k.sportasa, maxes.sportasa) +
cell(k.utakmica, maxes.utakmica) +
cell(k.sezona, 20) +
cell(k.trofeja, maxes.trofeja) +
cell(k.nagrada, maxes.nagrada) +
cell(k.u_ligama, maxes.u_ligama) +
`<td style="text-align:center">${logo}</td>` +
cell(k.godina_god, 18) +
`<td>${period}</td>` +
'</tr>';
});
html += '</tbody></table></div>';
c.innerHTML = html;
} catch(e) { c.innerHTML = '<div class="banner crit">Greška: '+e.message+'</div>'; }
}
async function pageAudit() {
// AUDIT_GUI_PATCH
const c = document.getElementById('content');
setTopbar('Audit', 'Status izvora podataka');
c.innerHTML = '<div class="card" style="padding:24px">Učitavam audit info…</div>';
try {
const [fresh, sources, coverage] = await Promise.all([
api('/api/v2/audit/freshness'),
api('/api/v2/audit/sources'),
api('/api/v2/audit/coverage')
]);
let html = `<div class="card" style="margin-bottom:14px">
<h2 style="margin:0">📊 Audit & izvori podataka</h2>
<p class="muted" style="font-size:11px">Posljednji upiti scrapera + distribucija izvora</p></div>`;
// Freshness
html += `<div class="card" style="margin-bottom:14px">
<h3 style="margin-bottom:10px">🕒 Aktualnost podataka</h3>
<table class="ri-tbl ri-sortable"><thead><tr><th>Tabela</th><th style="text-align:right">Broj</th><th>Zadnji update</th><th>Od zadnjeg</th></tr></thead><tbody>`;
fresh.freshness.forEach(f => {
const d = f.zadnji_update ? new Date(f.zadnji_update).toLocaleString('hr-HR') : '-';
const old = f.od_zadnjeg ? f.od_zadnjeg.split('.')[0] : '-';
const stale = f.zadnji_update && (Date.now() - new Date(f.zadnji_update)) > 7*86400000;
const route = auditRouteForTable(f.tabela);
html += `<tr style="cursor:pointer" onclick="${route}" title="Klik za otvaranje pregleda"><td><b>${f.tabela}</b></td><td style="text-align:right;color:var(--accent)">${(f.broj||0).toLocaleString('hr-HR')}</td><td class="mono" style="font-size:11px">${d}</td><td class="mono" style="${stale?'color:#ef4444':''};font-size:11px">${old}</td></tr>`;
});
html += `</tbody></table></div>`;
// Sources by tabela
html += `<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px">`;
html += `<div class="card"><h3 style="margin-bottom:10px">🏛️ Izvori klubova</h3>
<table class="ri-tbl ri-sortable"><thead><tr><th>Source</th><th style="text-align:right">Broj</th></tr></thead><tbody>`;
sources.klubovi_by_source.forEach(s => {
const r2 = sourceRoute('klubovi', s.source);
html += `<tr style="cursor:pointer" onclick="${r2}"><td><b>${s.source}</b></td><td style="text-align:right;color:var(--accent)">${s.broj.toLocaleString('hr-HR')}</td></tr>`;
});
html += `</tbody></table></div>`;
html += `<div class="card"><h3 style="margin-bottom:10px">👥 Izvori sportaši</h3>
<table class="ri-tbl ri-sortable"><thead><tr><th>Source</th><th style="text-align:right">Broj</th></tr></thead><tbody>`;
sources.clanovi_by_source.forEach(s => {
const r2 = sourceRoute('sportasi', s.source);
html += `<tr style="cursor:pointer" onclick="${r2}"><td><b>${s.source}</b></td><td style="text-align:right;color:var(--accent)">${s.broj.toLocaleString('hr-HR')}</td></tr>`;
});
html += `</tbody></table></div>`;
html += `<div class="card"><h3 style="margin-bottom:10px">⚽ Natjecanja</h3>
<table class="ri-tbl ri-sortable"><thead><tr><th>Source</th><th style="text-align:right">Broj</th></tr></thead><tbody>`;
sources.natjecanja_by_source.forEach(s => {
const r2 = sourceRoute('natjecanja', s.source);
html += `<tr style="cursor:pointer" onclick="${r2}"><td><b>${s.source}</b></td><td style="text-align:right;color:var(--accent)">${s.broj.toLocaleString('hr-HR')}</td></tr>`;
});
html += `</tbody></table></div>`;
html += `<div class="card"><h3 style="margin-bottom:10px">📄 Dokumenti</h3>
<table class="ri-tbl ri-sortable"><thead><tr><th>Vrsta</th><th style="text-align:right">Broj</th></tr></thead><tbody>`;
sources.dokumenti_by_vrsta.forEach(s => {
const r2 = sourceRoute('dokumenti', s.source);
html += `<tr style="cursor:pointer" onclick="${r2}"><td><b>${s.source}</b></td><td style="text-align:right;color:var(--accent)">${s.broj.toLocaleString('hr-HR')}</td></tr>`;
});
html += `</tbody></table></div>`;
html += `</div>`;
// Top klubovi by coverage
html += `<div class="card"><h3 style="margin-bottom:10px">🏆 Top klubovi po pokrivenosti (${coverage.count})</h3>
<table class="ri-tbl ri-sortable"><thead><tr><th>Klub</th><th>Sport</th><th style="text-align:right">Sportaša</th><th style="text-align:right">Utakmica</th><th style="text-align:right">Sezona</th><th style="text-align:right">Trofeja</th><th style="text-align:right">Nagrada</th><th>Source</th></tr></thead><tbody>`;
coverage.klubovi.slice(0, 100).forEach(k => {
html += `<tr style="cursor:pointer" onclick="state.klub_id=${k.id};goto('klubRoster')">
<td><b>${k.naziv}</b></td><td>${k.sport||'-'}</td>
<td style="text-align:right;color:var(--accent)">${k.sportasa||0}</td>
<td style="text-align:right">${k.utakmica||0}</td>
<td style="text-align:right">${k.sezona||0}</td>
<td style="text-align:right">${k.trofeja||0}</td>
<td style="text-align:right">${k.nagrada||0}</td>
<td class="mono" style="font-size:10px">${k.source||'-'}</td>
</tr>`;
});
html += `</tbody></table></div>`;
c.innerHTML = html;
} catch(e) { c.innerHTML = '<div class="banner crit">Greška: '+e.message+'</div>'; }
}
// MATRIX_PAGE - Audit Coverage Matrix heatmap
async function pageMatrix() {
const c = document.getElementById('content');
setTopbar('Coverage Matrix', 'Heat-map kvalitete podataka po klubu');
c.innerHTML = '<div class="card" style="padding:24px">Učitavam matricu…</div>';
try {
const params = new URLSearchParams(window.location.hash.split('?')[1] || '');
const minSize = params.get('min_size') || '5';
const sport = params.get('sport') || '';
const url = '/api/v2/audit/coverage-matrix?min_size=' + encodeURIComponent(minSize) + (sport ? '&sport=' + encodeURIComponent(sport) : '');
const r = await api(url);
const klubovi = r.klubovi || [];
// Sport filter dropdown
const sports = [...new Set(klubovi.map(k => k.sport).filter(Boolean))].sort();
// Color helper for heat
const heatColor = (v, max) => {
if (v === 0 || v == null) return 'var(--bg2)';
const t = Math.min(v / Math.max(max, 1), 1);
const r = Math.round(50 + (255-50) * (1-t));
const g = Math.round(50 + (220-50) * t);
return `rgb(${r},${g},80)`;
};
const scoreColor = (s) => {
if (s >= 70) return '#22c55e';
if (s >= 50) return '#eab308';
if (s >= 30) return '#f97316';
return '#ef4444';
};
// Max for each column for normalization
const maxSp = Math.max(...klubovi.map(k => k.sportasa || 0));
const maxUt = Math.max(...klubovi.map(k => k.utakmica || 0));
const maxTr = Math.max(...klubovi.map(k => k.trofeja || 0));
const maxGh = Math.max(...klubovi.map(k => k.god_hits || 0));
let html = `<div class="card" style="padding:18px;margin-bottom:14px">
<h2 style="margin:0 0 8px">📊 Coverage Matrix · Heat-map</h2>
<div class="muted" style="font-size:12px;margin-bottom:10px">${klubovi.length} klubova · weighted score (40% verified%, 20% utakmice, 15% sezona, 15% trofeja, 10% godišnjak hits)</div>
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center">
<span class="muted" style="font-size:12px">Min sportaša:</span>
${[1,5,10,20].map(n => `<button onclick="location.hash='matrix?min_size=${n}${sport?'&sport='+sport:''}'" style="padding:4px 10px;background:${minSize==n?'var(--accent)':'var(--bg2)'};border:1px solid var(--border);border-radius:4px;cursor:pointer;color:${minSize==n?'#000':'var(--text)'}">${n}+</button>`).join('')}
<span class="muted" style="font-size:12px;margin-left:12px">Sport:</span>
<select onchange="location.hash='matrix?min_size=${minSize}'+(this.value?'&sport='+encodeURIComponent(this.value):'')" style="padding:4px 8px;background:var(--bg2);border:1px solid var(--border);border-radius:4px;color:var(--text)">
<option value="">-- svi --</option>
${sports.map(s => `<option value="${s}" ${s===sport?'selected':''}>${s}</option>`).join('')}
</select>
</div>
</div>`;
// Color legend
html += `<div class="card" style="padding:10px;margin-bottom:10px;display:flex;gap:18px;flex-wrap:wrap;font-size:11px">
<span><b>Score:</b></span>
<span style="color:#22c55e">●</span> 70+ Excellent
<span style="color:#eab308">●</span> 50-69 Good
<span style="color:#f97316">●</span> 30-49 Sparse
<span style="color:#ef4444">●</span> &lt;30 Empty
</div>`;
// Matrix table
html += `<div class="card" style="padding:0;overflow:auto">
<table class="data-table" style="margin:0;font-size:11px;width:100%">
<thead style="position:sticky;top:0;background:var(--bg)">
<tr>
<th style="text-align:left">Klub</th>
<th>Sport</th>
<th>Grad</th>
<th title="Ukupno sportaša u DB">Sportaša</th>
<th title="Postotak verificiranih (source_url)">Verified%</th>
<th title="Utakmice u utakmice_log">Utakmica</th>
<th title="Sezona u clan_sezona">Sezona</th>
<th title="Trofeji u klub_sezona">Trofeji</th>
<th title="Nagrade u clan_nagrada">Nagrade</th>
<th title="Hits u godišnjacima 2006-2024">Godišnjaci</th>
<th title="Weighted overall score">Score</th>
</tr>
</thead><tbody>`;
for (const k of klubovi) {
html += `<tr style="cursor:pointer" onclick="location.hash='klub/${k.klub_id}'">
<td style="text-align:left;font-weight:600">${k.naziv}</td>
<td>${k.sport || '—'}</td>
<td class="muted">${k.grad || '—'}</td>
<td style="background:${heatColor(k.sportasa, maxSp)};color:#000;font-weight:600;text-align:right">${k.sportasa}</td>
<td style="background:${heatColor(k.verified_pct, 100)};color:#000;font-weight:600">${k.verified_pct}%</td>
<td style="background:${heatColor(k.utakmica, maxUt)};color:#000">${k.utakmica}</td>
<td>${k.sezona}</td>
<td style="background:${heatColor(k.trofeja, maxTr)};color:#000">${k.trofeja}</td>
<td>${k.nagrada}</td>
<td style="background:${heatColor(k.god_hits, maxGh)};color:#000">${k.god_hits}</td>
<td style="background:${scoreColor(k.score)};color:#000;font-weight:700;text-align:center">${Math.round(k.score)}</td>
</tr>`;
}
html += `</tbody></table></div>`;
// Sports breakdown
const bySport = {};
for (const k of klubovi) {
const s = k.sport || 'unknown';
bySport[s] = bySport[s] || {count:0, scoreSum:0, sportasa:0, verified:0};
bySport[s].count++;
bySport[s].scoreSum += k.score;
bySport[s].sportasa += k.sportasa;
bySport[s].verified += k.verified;
}
html += `<div class="card" style="padding:18px;margin-top:14px">
<h3 style="margin:0 0 10px">📊 Po sportu</h3>
<table class="data-table" style="font-size:12px">
<tr><th>Sport</th><th>Klubova</th><th>Sportaša</th><th>Verified</th><th>Avg Score</th></tr>
${Object.entries(bySport).sort((a,b) => b[1].scoreSum/b[1].count - a[1].scoreSum/a[1].count).map(([s, v]) =>
`<tr>
<td><a href="#matrix?min_size=${minSize}&sport=${encodeURIComponent(s)}">${s}</a></td>
<td>${v.count}</td>
<td>${v.sportasa}</td>
<td>${v.verified}</td>
<td style="background:${scoreColor(v.scoreSum/v.count)};color:#000;font-weight:600">${Math.round(v.scoreSum/v.count)}</td>
</tr>`
).join('')}
</table>
</div>`;
c.innerHTML = html;
} catch (e) {
c.innerHTML = `<div class="card" style="padding:18px;color:#f87171">Greška: ${e}</div>`;
console.error(e);
}
}
buildNavs();
// Read URL hash on load
const _initHash = window.location.hash.slice(1);
if (_initHash && NAV.flatMap(s=>s.items||[]).some(i=>i.id===_initHash)) {
goto(_initHash);
}
goto('dashboard');
checkRole();
</script>
<!-- Coverage Matrix page (Sprint 3) -->
<div id="pageCoverage" class="page" style="display:none">
<div style="padding:20px">
<h1 style="color:var(--accent);margin-bottom:8px">Coverage Matrix</h1>
<p style="color:var(--text2);margin-bottom:16px">Heat-map pokrivenosti podataka po klubu. Score 0-100: 40% verified%, 20% utakmice, 15% sezone, 15% trofeji, 10% godišnjak.</p>
<div style="display:flex;gap:12px;align-items:center;margin-bottom:16px;flex-wrap:wrap">
<label style="color:var(--text2)">Min veličina kluba: <input type="number" id="cmMinSize" value="10" min="1" max="100" style="width:60px;padding:4px;background:var(--card2);border:1px solid var(--border);color:var(--text);border-radius:4px"/></label>
<label style="color:var(--text2)">Sport: <input type="text" id="cmSport" placeholder="(svi)" style="width:140px;padding:4px;background:var(--card2);border:1px solid var(--border);color:var(--text);border-radius:4px"/></label>
<button onclick="loadCoverageMatrix()" style="padding:6px 14px;background:var(--accent);color:#000;border:none;border-radius:6px;cursor:pointer;font-weight:600">Učitaj</button>
<span id="cmCount" style="color:var(--text2)"></span>
</div>
<div id="cmTable" style="overflow-x:auto"></div>
</div>
</div>
<script>
async function loadCoverageMatrix() {
const min = document.getElementById('cmMinSize').value || 10;
const sport = document.getElementById('cmSport').value || '';
const params = new URLSearchParams({min_size: min});
if (sport) params.append('sport', sport);
document.getElementById('cmTable').innerHTML = '<div style="padding:20px;color:var(--text2)">Učitavam...</div>';
try {
const r = await fetch('/sport/api/v2/audit/coverage-matrix?' + params);
const d = await r.json();
document.getElementById('cmCount').innerText = d.count + ' klubova';
const klubovi = d.klubovi || [];
if (!klubovi.length) {
document.getElementById('cmTable').innerHTML = '<div style="padding:20px;color:var(--text2)">Nema rezultata.</div>';
return;
}
function cell(val, max) {
const pct = max > 0 ? Math.min(100, (val / max) * 100) : 0;
const hue = pct * 1.2; // 0=red, 120=green
const bg = `hsl(${hue}, 70%, 22%)`;
return `<td style="background:${bg};color:#fff;padding:6px 10px;text-align:center;font-weight:600">${val||0}</td>`;
}
function scoreCell(score) {
const hue = Math.min(120, score * 1.2);
const bg = `hsl(${hue}, 80%, 30%)`;
return `<td style="background:${bg};color:#fff;padding:6px 10px;text-align:center;font-weight:700;font-size:14px">${Math.round(score)}</td>`;
}
const maxes = {
sportasa: Math.max(...klubovi.map(k=>k.sportasa)),
verified: 100,
utakmica: Math.max(...klubovi.map(k=>k.utakmica)),
sezona: Math.max(...klubovi.map(k=>k.sezona), 1),
trofeja: Math.max(...klubovi.map(k=>k.trofeja), 1),
god: Math.max(...klubovi.map(k=>k.god_hits), 1),
};
let html = '<table style="width:100%;border-collapse:collapse;font-size:12px"><thead><tr style="background:var(--card2)">';
html += '<th style="padding:8px;text-align:left;color:var(--text2)">Klub</th>';
html += '<th style="padding:8px;color:var(--text2)">Sport</th>';
html += '<th style="padding:8px;color:var(--text2)">Sportaši</th>';
html += '<th style="padding:8px;color:var(--text2)">Verified%</th>';
html += '<th style="padding:8px;color:var(--text2)">Utakmica</th>';
html += '<th style="padding:8px;color:var(--text2)">Sezona</th>';
html += '<th style="padding:8px;color:var(--text2)">Trofeja</th>';
html += '<th style="padding:8px;color:var(--text2)">Godišnjak</th>';
html += '<th style="padding:8px;color:var(--text2)">Score</th>';
html += '</tr></thead><tbody>';
for (const k of klubovi) {
html += '<tr style="border-bottom:1px solid var(--border)">';
html += `<td style="padding:8px"><a href="#" onclick="loadKlub(${k.klub_id});showPage('pageKlubDetail');return false" style="color:var(--accent);text-decoration:none">${k.naziv}</a></td>`;
html += `<td style="padding:8px;color:var(--text2)">${k.sport||'-'}</td>`;
html += cell(k.sportasa, maxes.sportasa);
html += cell(k.verified_pct, maxes.verified);
html += cell(k.utakmica, maxes.utakmica);
html += cell(k.sezona, maxes.sezona);
html += cell(k.trofeja, maxes.trofeja);
html += cell(k.god_hits, maxes.god);
html += scoreCell(k.score);
html += '</tr>';
}
html += '</tbody></table>';
document.getElementById('cmTable').innerHTML = html;
} catch (e) {
document.getElementById('cmTable').innerHTML = '<div style="padding:20px;color:#f55">Greška: ' + e.message + '</div>';
}
}
// Load on page show
const _origShowPageCM = typeof showPage === 'function' ? showPage : null;
window.addEventListener('load', () => {
// hook
const orig = window.showPage;
if (orig && !window._cmHooked) {
window._cmHooked = true;
window.showPage = function(pid) {
orig(pid);
if (pid === 'pageCoverage' && !window._cmLoaded) {
window._cmLoaded = true;
loadCoverageMatrix();
}
};
}
});
// === Document Viewer Modal ===
async function showDokViewerModal(docId) {
try {
const res = await fetch('/sport/api/v2/dokumenti/' + docId).then(r=>r.json());
const doc = res.data || res;
if (!doc) { alert('Dokument nije pronađen'); return; }
// Build modal
let bd = document.querySelector('.ri-modal');
if (bd) bd.remove();
bd = document.createElement('div');
bd.className = 'ri-modal';
bd.onclick = (e) => { if (e.target === bd) closeRiModal(); };
const formatList = [];
if (doc.url || doc.izvor_url || doc.pdf_url) formatList.push({label:'Originalni URL', url: doc.url || doc.izvor_url || doc.pdf_url, icon:iconExternal()});
if (doc.fname && doc.fname.endsWith('.pdf')) formatList.push({label:'PDF lokalno', url: '/sport/_files/' + doc.fname, icon:iconDownload()});
formatList.push({label:'Parsirani tekst', url: '/sport/api/v2/dokumenti/' + docId + '/text', icon:iconFile()});
bd.innerHTML = `
<div class="ri-modal-box" style="max-width:1200px;width:96%">
<div class="ri-modal-h">
<div style="flex:1">
<div style="font-weight:600;font-size:16px">${doc.title || doc.fname || 'Dokument #' + docId}</div>
<div class="muted" style="font-size:11px;margin-top:3px">
${doc.vrsta || ''} ${doc.organizacija ? '· ' + doc.organizacija : ''} ${doc.godina ? '· ' + doc.godina : ''}
${doc.sadrzaj ? '· ' + (doc.sadrzaj.length).toLocaleString('hr-HR') + ' znakova' : ''}
</div>
</div>
<button class="ri-icon-btn" onclick="closeRiModal()">${iconX()}</button>
</div>
<div class="ri-modal-body" style="padding:18px">
<div style="display:flex;gap:10px;margin-bottom:18px;flex-wrap:wrap">
${formatList.map(f => `<a href="${f.url}" target="_blank" class="ri-icon-btn" style="text-decoration:none;color:var(--text);padding:8px 14px;border:1px solid var(--border-1);border-radius:6px;display:inline-flex;align-items:center;gap:6px">
${f.icon} ${f.label}
</a>`).join('')}
</div>
${doc.kratak_opis ? `<div style="padding:14px;background:var(--bg-2);border-radius:8px;margin-bottom:18px">${doc.kratak_opis}</div>` : ''}
${doc.sadrzaj ? `<div style="font-family:monospace;font-size:12px;background:var(--bg-2);padding:14px;border-radius:8px;max-height:60vh;overflow-y:auto;white-space:pre-wrap;line-height:1.5">${doc.sadrzaj.substring(0,30000)}${doc.sadrzaj.length > 30000 ? '\n\n... (skraćeno, ' + (doc.sadrzaj.length - 30000).toLocaleString('hr-HR') + ' znakova više)' : ''}</div>` : '<div class="muted">Nema parsiranog teksta.</div>'}
</div>
</div>`;
document.body.appendChild(bd);
} catch(e) {
alert('Greška: ' + e);
}
}
// === Document list modal (filtered) ===
async function showDokListModal(filter, title) {
try {
let url = '/sport/api/v2/dokumenti?limit=200';
if (filter && filter.vrsta) url += '&vrsta=' + encodeURIComponent(filter.vrsta);
if (filter && filter.sport) url += '&sport=' + encodeURIComponent(filter.sport);
const res = await fetch(url).then(r=>r.json());
const docs = (res.data || res || []);
let bd = document.querySelector('.ri-modal');
if (bd) bd.remove();
bd = document.createElement('div');
bd.className = 'ri-modal';
bd.onclick = (e) => { if (e.target === bd) closeRiModal(); };
bd.innerHTML = `
<div class="ri-modal-box" style="max-width:1200px;width:96%">
<div class="ri-modal-h">
<div style="flex:1">${title || 'Dokumenti'} <span class="muted" style="font-size:12px">(${docs.length})</span></div>
<button class="ri-icon-btn" onclick="closeRiModal()">${iconX()}</button>
</div>
<div class="ri-modal-body" style="padding:0;max-height:75vh;overflow-y:auto">
<table class="ri-tbl ri-sortable" style="margin:0">
<thead><tr><th>Naslov</th><th>Vrsta</th><th>Org.</th><th>Godina</th><th style="text-align:right">Veličina</th><th style="text-align:right">Akcije</th></tr></thead>
<tbody>
${docs.map(d => `<tr style="cursor:pointer" onclick="showDokViewerModal(${d.id})">
<td><b>${d.title || d.fname || ('#'+d.id)}</b></td>
<td><span class="badge">${d.vrsta || '-'}</span></td>
<td>${d.organizacija || '-'}</td>
<td class="mono">${d.godina || '-'}</td>
<td class="mono" style="text-align:right">${d.sadrzaj_len ? d.sadrzaj_len.toLocaleString('hr-HR') : '-'}</td>
<td style="text-align:right;white-space:nowrap" onclick="event.stopPropagation()">
<button class="ri-icon-btn-sm" onclick="showDokViewerModal(${d.id})" title="Pregled">${iconEye()}</button>
${(d.url||d.izvor_url||d.pdf_url) ? `<a href="${d.url||d.izvor_url||d.pdf_url}" target="_blank" class="ri-icon-btn-sm" title="Originalni URL">${iconExternal()}</a>` : ''}
<a href="/sport/api/v2/dokumenti/${d.id}/text" target="_blank" class="ri-icon-btn-sm" title="Parsirani tekst">${iconFile()}</a>
</td>
</tr>`).join('')}
</tbody>
</table>
</div>
</div>`;
document.body.appendChild(bd);
autoAttachSort();
} catch(e) {
alert('Greška: ' + e);
}
}
// === AUDIT HELPERS ===
function auditRouteForTable(tabela) {
// 'klubovi (HBS)' / 'klubovi (HNS)' / 'clanovi' / 'natjecanja' / 'dokumenti' / 'utakmice_log'
const t = (tabela || '').toLowerCase();
if (t.startsWith('klubovi')) return "goto('klubovi')";
if (t.startsWith('clan')) return "goto('clanovi')";
if (t.startsWith('clan_nagrada')) return "goto('kategorije')";
if (t.startsWith('clan_sezona')) return "goto('kategorije')";
if (t.startsWith('natjecanja_tablice')) return "goto('natjecanja')";
if (t.startsWith('natjecanja')) return "goto('natjecanja')";
if (t.startsWith('dokumenti')) return "goto('dokumenti')";
if (t.startsWith('utakmice')) return "goto('natjecanja')";
return "goto('dashboard')";
}
function sourceRoute(scope, source) {
// Open filtered list of items by source
const src = encodeURIComponent(source || '');
if (scope === 'klubovi') return `state.scrapeFilter='${src}';goto('klubovi')`;
if (scope === 'sportasi') return `state.sourceFilter='${src}';goto('sportasi')`;
if (scope === 'natjecanja') return `state.natjecanjeSource='${src}';goto('natjecanja')`;
if (scope === 'dokumenti') return `showDokListModal({vrsta:'${src}'},'Dokumenti: ${src}')`;
return `goto('${scope}')`;
}
// === Card/Table view toggle helper ===
window.viewModes = window.viewModes || {};
function viewToggle(scope, currentMode) {
window.viewModes[scope] = currentMode === 'card' ? 'table' : 'card';
// Re-render current page
if (typeof render === 'function') render();
}
function viewToggleHTML(scope, defaultMode) {
const current = window.viewModes[scope] || defaultMode || 'table';
return `<div class="ri-vtoggle" style="display:inline-flex;background:var(--bg-2);border:1px solid var(--border-1);border-radius:6px;overflow:hidden;margin-left:auto">
<button class="ri-vtoggle-btn ${current==='table'?'active':''}" onclick="window.viewModes['${scope}']='table';if(typeof render==='function')render()" title="Tablica" style="padding:5px 10px;background:${current==='table'?'var(--accent)':'transparent'};color:${current==='table'?'#fff':'var(--text)'};border:none;cursor:pointer">${iconTable()}</button>
<button class="ri-vtoggle-btn ${current==='card'?'active':''}" onclick="window.viewModes['${scope}']='card';if(typeof render==='function')render()" title="Kartice" style="padding:5px 10px;background:${current==='card'?'var(--accent)':'transparent'};color:${current==='card'?'#fff':'var(--text)'};border:none;cursor:pointer">${iconCards()}</button>
</div>`;
}
function iconTable() { return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/></svg>'; }
function iconCards() { return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>'; }
// === MISSING HELPER FUNCTIONS (added 2026-05-02) ===
function spAddNew() {
// Open simple new sportaš modal — user fills in essentials
const html = `
<div class="ri-modal" id="riModal" onclick="if(event.target===this)closeRiModal()">
<div class="ri-modal-box" style="max-width:680px">
<div class="ri-modal-h"><h3 style="margin:0">Novi sportaš</h3>
<button class="ri-icon-btn" onclick="closeRiModal()">${iconX()}</button></div>
<div class="ri-modal-body" style="padding:18px;overflow-y:auto;max-height:75vh">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px">
<div><label class="muted">Ime *</label><input id="spIme" class="inp" required></div>
<div><label class="muted">Prezime *</label><input id="spPrezime" class="inp" required></div>
<div><label class="muted">Spol</label>
<select id="spSpol" class="inp"><option value="">(nepoznat)</option><option value="M">M</option><option value="Ž">Ž</option></select></div>
<div><label class="muted">Sport</label>
<input id="spSport" class="inp" placeholder="nogomet, košarka..."></div>
<div><label class="muted">Datum rođenja</label><input id="spDob" type="date" class="inp"></div>
<div><label class="muted">Mjesto rođenja</label><input id="spMjesto" class="inp"></div>
<div><label class="muted">Klub</label>
<select id="spKlub" class="inp"><option value="">(nije izabran)</option></select></div>
<div><label class="muted">Uloga</label>
<select id="spUloga" class="inp">
<option value="igrac">igrač</option><option value="trener">trener</option>
<option value="kondicioni_trener">kondicioni trener</option>
<option value="predsjednik">predsjednik</option><option value="tajnik">tajnik</option>
<option value="direktor">direktor</option><option value="fizioterapeut">fizioterapeut</option>
<option value="lijecnik">liječnik</option><option value="sudac">sudac</option>
<option value="ostalo">ostalo</option>
</select></div>
<div style="grid-column:1/-1"><label class="muted">Slika (URL ili upload)</label>
<input id="spSlikaUrl" class="inp" placeholder="https://..."></div>
<div style="grid-column:1/-1"><label class="muted">Source URL (obavezno za datum_rodjenja!)</label>
<input id="spSourceUrl" class="inp" placeholder="https://..."></div>
<div style="grid-column:1/-1"><label class="muted">Napomena</label>
<textarea id="spNapomena" class="inp" rows="2"></textarea></div>
</div>
<div style="display:flex;justify-content:flex-end;gap:8px">
<button class="btn" onclick="closeRiModal()">Odustani</button>
<button class="btn primary" onclick="spSubmitNew()">Spremi</button>
</div>
</div>
</div>
</div>`;
const div = document.createElement('div'); div.innerHTML = html;
document.body.appendChild(div.firstChild);
// Load klubovi for dropdown
fetch('/sport/api/v2/klubovi/sa-clanstvom?limit=500').then(r=>r.json()).then(d => {
const sel = document.getElementById('spKlub');
if (!sel) return;
(d.data || d.results || d || []).forEach(k => {
const opt = document.createElement('option');
opt.value = k.id; opt.textContent = k.naziv + (k.sport ? ' ('+k.sport+')' : '');
sel.appendChild(opt);
});
}).catch(()=>{});
}
async function spSubmitNew() {
const data = {
ime: document.getElementById('spIme').value.trim(),
prezime: document.getElementById('spPrezime').value.trim(),
spol: document.getElementById('spSpol').value || null,
sport: document.getElementById('spSport').value.trim() || null,
datum_rodjenja: document.getElementById('spDob').value || null,
mjesto_rodjenja: document.getElementById('spMjesto').value.trim() || null,
klub_id: document.getElementById('spKlub').value || null,
uloga: document.getElementById('spUloga').value,
slika_url: document.getElementById('spSlikaUrl').value.trim() || null,
source_url: document.getElementById('spSourceUrl').value.trim() || null,
napomena: document.getElementById('spNapomena').value.trim() || null
};
if (!data.ime || !data.prezime) { alert('Ime i prezime su obavezni'); return; }
if (data.datum_rodjenja && !data.source_url) {
alert('Datum rođenja zahtijeva source_url (DB policy).'); return;
}
try {
const res = await fetch('/sport/api/v2/sportas/create', {
method: 'POST',
headers: {'Content-Type':'application/json', 'Authorization':'Bearer '+(localStorage.getItem('rinet_v2_token')||'')},
body: JSON.stringify(data)
});
if (!res.ok) throw new Error('API ' + res.status + ': ' + await res.text());
const r = await res.json();
closeRiModal();
alert('Sportaš #' + (r.id || r.data?.id || '?') + ' kreiran');
if (typeof render === 'function') render();
} catch(e) { alert('Greška: ' + e.message); }
}
// Close modal helper (alias if not defined)
if (typeof closeRiModal !== 'function') {
window.closeRiModal = function(){
const m = document.getElementById('riModal'); if (m) m.remove();
};
}
// === GOOGLE AI ENRICHMENT POPUP ===
async function showEnrichModal(entityType, entityId, entityName, query) {
// entityType: 'klub' | 'manifestacija' | 'sportas' | 'objekt' | 'osoba' etc.
// entityName/query: natural language search query
let bd = document.querySelector('.ri-modal');
if (bd) bd.remove();
bd = document.createElement('div');
bd.className = 'ri-modal';
bd.id = 'riModal';
bd.onclick = (e) => { if (e.target === bd) closeRiModal(); };
bd.innerHTML = `
<div class="ri-modal-box" style="max-width:900px">
<div class="ri-modal-h">
<div style="flex:1">
<div style="font-weight:600">${entityName}</div>
<div class="muted" style="font-size:11px">${entityType} · AI obogaćeno</div>
</div>
<button class="ri-icon-btn" onclick="closeRiModal()">${iconX()}</button>
</div>
<div class="ri-modal-body" id="enrichBody" style="padding:20px">
<div style="text-align:center;padding:30px">
<div class="muted">⏳ Pretražujem internet i obogaćujem podatke o "${entityName}"...</div>
<div style="margin-top:14px;font-size:11px;color:var(--text3)">Ovo može potrajati 5-15 sekundi</div>
</div>
</div>
</div>`;
document.body.appendChild(bd);
try {
const res = await fetch('/sport/api/v2/enrich/google-ai', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({entity_type: entityType, entity_id: entityId, query: query || entityName})
});
if (!res.ok) throw new Error('API ' + res.status);
const data = await res.json();
const body = document.getElementById('enrichBody');
if (!body) return;
let h = '';
if (data.summary) {
h += `<div style="background:var(--bg-2);padding:16px;border-radius:8px;margin-bottom:14px;line-height:1.6">${data.summary.replace(/\n/g,'<br>')}</div>`;
}
if (data.facts && data.facts.length) {
h += '<div style="margin-bottom:14px"><b style="color:var(--accent)">Ključne činjenice:</b><ul style="margin-top:8px">';
data.facts.forEach(f => h += `<li style="margin-bottom:6px">${f}</li>`);
h += '</ul></div>';
}
if (data.sources && data.sources.length) {
h += '<div style="border-top:1px solid var(--border-1);padding-top:14px;margin-top:14px"><b style="color:var(--text3);font-size:11px">IZVORI:</b><div style="margin-top:8px;display:flex;flex-direction:column;gap:6px">';
data.sources.forEach(s => {
h += `<a href="${s.url}" target="_blank" class="muted" style="font-size:12px;text-decoration:none;color:var(--accent);display:flex;align-items:center;gap:6px">${iconExternal()} ${s.title || s.url}</a>`;
});
h += '</div></div>';
}
if (data.saved_to_db) {
h += '<div style="margin-top:14px;padding:8px 12px;background:rgba(56,180,116,0.15);border-left:3px solid #38b474;font-size:12px;color:#38b474">✓ Spremljeno u bazu znanja (RAG embeddings)</div>';
}
if (data.google_search_url) {
h += `<div style="margin-top:14px;text-align:right"><a href="${data.google_search_url}" target="_blank" class="btn" style="text-decoration:none;display:inline-flex;align-items:center;gap:6px">${iconExternal()} Google search</a></div>`;
}
body.innerHTML = h || '<div class="muted">Nema podataka.</div>';
} catch(e) {
const body = document.getElementById('enrichBody');
if (body) body.innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>';
}
}
// Klik handler za "obogaćivanje" - kad nešto nema podatke
function enrichClick(type, id, name) {
showEnrichModal(type, id, name, name);
}
// Helper - shown at top of search results to enrich
function searchEnrichBtn(query) {
return `<button class="btn primary" onclick="showEnrichModal('search', null, ${JSON.stringify(query)}, ${JSON.stringify(query)})" style="display:flex;align-items:center;gap:6px">
${iconExternal()} AI obogati pretragu (Internet + LLM)
</button>`;
}
// Helper - shown on every entity that lacks data
function entityEnrichBtn(type, id, name) {
return `<button class="ri-icon-btn-sm" onclick="showEnrichModal('${type}', ${id || 'null'}, ${JSON.stringify(name)}, ${JSON.stringify(name)})" title="AI obogati podatke">
🔍 ${iconExternal()}
</button>`;
}
// Force-fresh reload helper (Ctrl+R doesn't always invalidate inline JS)
window.forceReload = function() {
const url = new URL(window.location.href);
url.searchParams.set('_v', Date.now());
window.location.href = url.toString();
};
// === KLUB WEB ENRICH HELPER ===
async function klubWebEnrich(klubId, klubNaziv) {
if (!confirm('Pokrenuti AI obogaćivanje za "' + klubNaziv + '" iz njihove web stranice?\n\nTo će dohvatiti uprava + stručni stožer + igrače sa klubske web stranice i upisati ih u bazu sa odgovarajućim ulogama.')) return;
let bd = document.querySelector('.ri-modal');
if (bd) bd.remove();
bd = document.createElement('div');
bd.className = 'ri-modal';
bd.id = 'riModal';
bd.onclick = (e) => { if (e.target === bd) closeRiModal(); };
bd.innerHTML = `
<div class="ri-modal-box" style="max-width:600px">
<div class="ri-modal-h"><div style="flex:1"><b>AI Obogaćivanje kluba</b><br><span class="muted" style="font-size:11px">${klubNaziv}</span></div>
<button class="ri-icon-btn" onclick="closeRiModal()">${iconX()}</button></div>
<div class="ri-modal-body" id="enrichBody" style="padding:24px;text-align:center">
<div class="muted">⏳ Dohvaćam podatke s web stranice...<br><span style="font-size:11px">Može potrajati 30-60 sekundi</span></div>
</div>
</div>`;
document.body.appendChild(bd);
try {
const res = await fetch('/sport/api/v2/enrich/klub-web', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({klub_id: klubId})
});
const data = await res.json();
const body = document.getElementById('enrichBody');
if (!body) return;
if (data.error) {
body.innerHTML = '<div class="banner crit">' + data.error + '</div><div class="muted" style="font-size:11px;margin-top:8px">Možda klub nema web URL u bazi. Edit klub i dodaj web URL.</div>';
return;
}
let h = '<div style="text-align:left">';
h += '<div style="background:rgba(56,180,116,0.15);padding:14px;border-left:3px solid #38b474;border-radius:6px;margin-bottom:14px">';
h += '<div><b>✓ Obogaćivanje uspješno</b></div>';
h += '<div style="margin-top:8px">' + (data.people_count || 0) + ' osoba prepoznatih</div>';
h += '<div>Novih: <b>' + (data.inserted||0) + '</b></div>';
h += '<div>Ažuriranih: <b>' + (data.updated||0) + '</b></div>';
if (data.skipped) h += '<div>Preskočeno: ' + data.skipped + '</div>';
h += '</div>';
if (data.by_uloga) {
h += '<div style="margin-bottom:14px"><b>Po ulogama:</b><div style="margin-top:6px">';
Object.entries(data.by_uloga).forEach(([u, c]) => {
h += '<span class="badge" style="margin:2px">' + u + ': <b>' + c + '</b></span>';
});
h += '</div></div>';
}
if (data.fetched_urls) {
h += '<div style="border-top:1px solid var(--border-1);padding-top:12px"><b style="font-size:11px;color:var(--text3)">IZVORI:</b>';
data.fetched_urls.forEach(u => {
h += '<div style="margin-top:4px"><a href="' + u + '" target="_blank" style="font-size:11px;color:var(--accent);text-decoration:none">' + iconExternal() + ' ' + u + '</a></div>';
});
h += '</div>';
}
h += '<div style="margin-top:18px;text-align:right"><button class="btn primary" onclick="closeRiModal();render()">OK · Osvježi</button></div>';
h += '</div>';
body.innerHTML = h;
} catch(e) {
const body = document.getElementById('enrichBody');
if (body) body.innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>';
}
}
// Auto-inject klubWebEnrich button on klub roster page
(function() {
const origRender = window.render;
if (origRender && !window._klubBtnHook) {
window._klubBtnHook = true;
const obs = new MutationObserver(() => {
if (window.state && window.state.page === 'klubRoster' && window.state.klub_id) {
const breadcrumb = document.querySelector('.breadcrumbs') || document.querySelector('h1');
if (breadcrumb && !document.getElementById('klubEnrichBtn')) {
const btn = document.createElement('button');
btn.id = 'klubEnrichBtn';
btn.className = 'btn primary';
btn.style.cssText = 'margin-left:14px;display:inline-flex;align-items:center;gap:6px;font-size:12px;padding:6px 12px';
btn.innerHTML = '🔄 Obogati iz weba';
btn.onclick = () => {
const naziv = (document.querySelector('h1') || {}).textContent || 'klub';
klubWebEnrich(state.klub_id, naziv);
};
(breadcrumb.parentNode || breadcrumb).appendChild(btn);
}
}
});
obs.observe(document.getElementById('content') || document.body, {childList:true, subtree:true});
}
})();
// Delegated handler for chat enrich buttons (avoid inline onclick escape issues)
document.addEventListener('click', function(e) {
const btn = e.target.closest('.chat-enrich-btn');
if (btn) {
const q = btn.getAttribute('data-q') || '';
if (q && typeof showEnrichModal === 'function') {
showEnrichModal('search', null, q, q);
}
}
});
// === KLUB PICKER WITH AUTOCOMPLETE ===
function attachKlubPicker(inputId, onSelectCallback) {
const inp = document.getElementById(inputId);
if (!inp) return;
// Wrap input in container if not already
let wrap = inp.closest('.klub-picker-wrap');
if (!wrap) {
wrap = document.createElement('div');
wrap.className = 'klub-picker-wrap';
wrap.style.cssText = 'position:relative;display:inline-block;width:100%;max-width:400px';
inp.parentNode.insertBefore(wrap, inp);
wrap.appendChild(inp);
}
// Suggest dropdown
let drop = wrap.querySelector('.klub-suggest');
if (!drop) {
drop = document.createElement('div');
drop.className = 'klub-suggest';
drop.style.cssText = 'display:none;position:absolute;top:100%;left:0;right:0;max-height:280px;overflow-y:auto;background:var(--bg-1);border:1px solid var(--border-1);border-radius:6px;z-index:100;margin-top:2px;box-shadow:0 4px 16px rgba(0,0,0,0.4)';
wrap.appendChild(drop);
}
inp.setAttribute('placeholder', 'Upiši ime kluba (npr. Zamet, Rijeka...)');
let timer = null;
inp.oninput = function() {
clearTimeout(timer);
const q = inp.value.trim();
if (!q || q.length < 2) { drop.style.display = 'none'; return; }
timer = setTimeout(async () => {
try {
const r = await fetch('/sport/api/v2/klubovi?q=' + encodeURIComponent(q) + '&limit=20');
const d = await r.json();
const items = d.data || d.results || d || [];
if (!items.length) { drop.style.display = 'none'; return; }
drop.innerHTML = items.map(k =>
`<div class="klub-suggest-item" data-id="${k.id}" data-naziv="${(k.naziv||'').replace(/"/g,'&quot;')}"
style="padding:8px 12px;cursor:pointer;border-bottom:1px solid var(--border-2);font-size:13px">
<b>${k.naziv}</b> ${k.sport ? '<span class="muted" style="font-size:11px">· '+k.sport+'</span>':''}
${k.grad ? '<span class="muted" style="font-size:11px"> · '+k.grad+'</span>':''}
<span style="color:var(--text3);font-size:10px;float:right">#${k.id}</span>
</div>`).join('');
drop.style.display = 'block';
drop.querySelectorAll('.klub-suggest-item').forEach(el => {
el.onclick = () => {
const kid = parseInt(el.getAttribute('data-id'));
const naziv = el.getAttribute('data-naziv');
inp.value = naziv + ' (#' + kid + ')';
inp.setAttribute('data-klub-id', kid);
drop.style.display = 'none';
if (onSelectCallback) onSelectCallback(kid, naziv);
};
el.onmouseover = () => el.style.background = 'var(--bg-2)';
el.onmouseout = () => el.style.background = 'transparent';
});
} catch(e) { drop.style.display = 'none'; }
}, 200);
};
inp.onblur = () => setTimeout(() => { drop.style.display = 'none'; }, 200);
}
// Helper: extract klub_id from picker input
function getPickerKlubId(inputId) {
const inp = document.getElementById(inputId);
if (!inp) return null;
// Check data attribute first (was selected)
const did = inp.getAttribute('data-klub-id');
if (did) return parseInt(did);
// Otherwise try parse "Klub Name (#123)" pattern
const m = (inp.value || '').match(/#(\d+)/);
if (m) return parseInt(m[1]);
return null;
}
// Auto-sort klub roster table by uloga group
(function() {
if (window._klubRosterSortHook) return;
window._klubRosterSortHook = true;
const ULOGA_ORDER = {
'predsjednik': 1, 'dopredsjednik': 2, 'tajnik': 3, 'direktor': 4,
'član uprave': 5, 'član nadzornog odbora': 6,
'trener': 10, 'pomocni_trener': 11, 'trener_vratara': 12,
'kondicioni_trener': 13, 'fizioterapeut': 14, 'lijecnik': 15,
'team_manager': 16, 'analiticar': 17, 'video_analiticar': 18,
'igrac': 50, 'sportaš': 51, 'sportas': 51,
'sudac': 60, 'ostalo': 90, '': 99
};
// No client-side reorder needed - depends on data structure
})();
// === OBRASCI SPORTSKIH SAVEZA (RSS, ZSP PGŽ, HOO, Lokalni savezi) ===
window.OBRASCI_KATALOG = [
// RSS Rijeka (Riječki sportski savez)
{ id:'rss-prijava-godisnjak', naziv:'Prijava za Godišnjak ZS PGŽ', savez:'RSS Rijeka / ZS PGŽ', kategorija:'Godisnji', polja:['Naziv kluba','Sport','OIB','Predsjednik','Tajnik','Adresa','Email','Web','Broj članova M','Broj članova Ž','Broj kategoriziranih','Broj reprezentativaca','Trofeji 2025','Najveći uspjeh'] },
{ id:'rss-zahtjev-program', naziv:'Zahtjev za sufinanciranje programa sporta', savez:'RSS Rijeka', kategorija:'Financiranje', polja:['Klub','OIB','Naziv programa','Iznos zahtjeva','Razdoblje od','Razdoblje do','Broj polaznika','Cilj programa','Voditelj','Telefon'] },
{ id:'rss-natjecanje', naziv:'Prijava manifestacije / natjecanja', savez:'RSS Rijeka', kategorija:'Manifestacija', polja:['Klub organizator','Naziv manifestacije','Mjesto','Datum','Razina (lokalna/županijska/državna/međunarodna)','Broj sudionika očekivanih','Sport','Voditelj','Mobitel','Email'] },
// ZS PGŽ
{ id:'zs-pgz-godisnji-izvjestaj', naziv:'Godišnji izvještaj kluba ZS PGŽ', savez:'ZS PGŽ', kategorija:'Godisnji', polja:['Klub','OIB','Sport','Predsjednik','Broj članova ukupno','Broj M','Broj Ž','Broj kategoriziranih HOO','Broj reprezentativaca','Najveći uspjesi 2025','Plan 2026'] },
{ id:'zs-pgz-najuspjesniji', naziv:'Prijedlog za nagradu ZS PGŽ "Najuspješniji"', savez:'ZS PGŽ', kategorija:'Nagrade', polja:['Kategorija (sportaš godine, ekipa godine, trener, životno djelo)','Predloženik','Klub','Sport','Obrazloženje','Postignuće 2025'] },
{ id:'zs-pgz-stipendija', naziv:'Zahtjev za sportsku stipendiju PGŽ', savez:'PGŽ', kategorija:'Stipendije', polja:['Sportaš ime i prezime','OIB','Datum rođenja','Klub','Sport','Kategorija HOO','Reprezentacija','Najveći uspjesi','Iznos zahtjeva','IBAN','Suglasnost roditelja (PDF)'] },
// PGŽ proračun
{ id:'pgz-prijava-natjecaj', naziv:'Prijava na javni natječaj PGŽ za sport', savez:'PGŽ', kategorija:'Financiranje', polja:['Naziv kluba','OIB','Predsjednik','Adresa','Iznos zahtjeva','Aktivnost','Razdoblje','Voditelj','IBAN','Banka'] },
// Putni nalog (klubsko interno)
{ id:'klub-putni-nalog', naziv:'Putni nalog za klupskog djelatnika', savez:'Interno klub', kategorija:'Operativa', polja:['Klub','Ime i prezime','Funkcija','Vrsta vozila','Polazište','Odredište','Datum polaska','Datum povratka','Svrha putovanja','Iznos predujma EUR'] },
// Liječnička potvrda
{ id:'lijecnicki-pregled', naziv:'Zahtjev za sportsko-liječnički pregled (ZZJZ PGŽ)', savez:'ZZJZ PGŽ', kategorija:'Medicina', polja:['Sportaš','OIB','Klub','Sport','Datum rođenja','Vrsta pregleda (osnovni/specijalistički)','Datum željen','Telefon','Email'] },
// Kategorizacija HOO
{ id:'hoo-kategorizacija', naziv:'Prijava za HOO kategorizaciju sportaša', savez:'HOO (preko saveza)', kategorija:'Kategorizacija', polja:['Sportaš','OIB','Datum rođenja','Klub','Sport','Disciplina','Prijedlog kategorije (I/II/III)','Postignuća (godina rezultat)','Reprezentacija (Da/Ne)'] },
// Klub osnivanje
{ id:'osnivanje-kluba', naziv:'Zahtjev za upis kluba u registar saveza', savez:'Lokalni savez', kategorija:'Klub', polja:['Naziv kluba','Skraćeni naziv','OIB','Adresa','Predsjednik','OIB predsjednika','Tajnik','Sport','Datum osnivačke skupštine','Broj članova osnivača'] }
];
async function pageObrasci() {
// Deprecated - redirect to backend-driven forms page
if (typeof pageForms === 'function') return pageForms();
state.page = 'forms';
setTopbar('Obrasci', 'Backend templates');
}
function openObrazac(id) {
const obr = OBRASCI_KATALOG.find(o => o.id === id);
if (!obr) return;
let bd = document.querySelector('.ri-modal');
if (bd) bd.remove();
bd = document.createElement('div');
bd.className = 'ri-modal';
bd.id = 'riModal';
bd.onclick = (e) => { if (e.target === bd) closeRiModal(); };
let formHtml = '<div class="ri-modal-box" style="max-width:720px">';
formHtml += `<div class="ri-modal-h"><div style="flex:1"><b>${obr.naziv}</b><br><span class="muted" style="font-size:11px">${obr.savez} · ${obr.kategorija}</span></div>
<button class="ri-icon-btn" onclick="closeRiModal()">${iconX()}</button></div>`;
formHtml += '<div class="ri-modal-body" style="padding:20px;overflow-y:auto;max-height:75vh">';
formHtml += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px">';
obr.polja.forEach((p, i) => {
const isLong = p.toLowerCase().includes('obraz') || p.toLowerCase().includes('cilj') || p.toLowerCase().includes('uspj') || p.toLowerCase().includes('plan');
if (isLong) {
formHtml += `<div style="grid-column:1/-1"><label class="muted" style="font-size:11px">${p}</label><textarea id="obr_${i}" class="inp" rows="3" style="width:100%"></textarea></div>`;
} else {
formHtml += `<div><label class="muted" style="font-size:11px">${p}</label><input id="obr_${i}" class="inp" style="width:100%"></div>`;
}
});
formHtml += '</div>';
formHtml += '<div style="margin-top:18px;display:flex;justify-content:flex-end;gap:8px">';
formHtml += `<button class="btn" onclick="obrazacPrint('${obr.id}')">🖨 Print</button>`;
formHtml += `<button class="btn" onclick="obrazacSavePDF('${obr.id}')">📥 PDF</button>`;
formHtml += `<button class="btn primary" onclick="obrazacSubmit('${obr.id}')">📤 Pošalji ${obr.savez.split(' ')[0]}</button>`;
formHtml += '</div></div></div>';
bd.innerHTML = formHtml;
document.body.appendChild(bd);
}
async function obrazacSubmit(id) {
const obr = OBRASCI_KATALOG.find(o => o.id === id);
const data = {};
obr.polja.forEach((p, i) => {
data[p] = (document.getElementById('obr_'+i) || {}).value || '';
});
try {
const r = await fetch('/sport/api/v2/obrasci/submit', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({obrazac_id: id, naziv: obr.naziv, savez: obr.savez, data: data})
});
if (!r.ok) throw new Error('API ' + r.status);
const res = await r.json();
alert('✓ Obrazac spremljen #' + (res.id || '?') + '\nPrimit će ga: ' + obr.savez);
closeRiModal();
} catch(e) {
alert('Greška: ' + e.message + '\n\nObrazac trenutno nije implementiran na backendu - generiram PDF lokalno.');
obrazacSavePDF(id);
}
}
function obrazacPrint(id) { window.print(); }
function obrazacSavePDF(id) {
// Use browser print to PDF
const obr = OBRASCI_KATALOG.find(o => o.id === id);
let html = '<html><head><title>' + obr.naziv + '</title><style>body{font-family:sans-serif;padding:30px}h1{color:#1e40af}label{display:block;margin-top:14px;font-weight:600}.val{padding:6px;border-bottom:1px solid #999;min-height:20px}</style></head><body>';
html += '<h1>' + obr.naziv + '</h1>';
html += '<p><b>Savez:</b> ' + obr.savez + '<br><b>Datum:</b> ' + new Date().toLocaleDateString('hr-HR') + '</p>';
obr.polja.forEach((p, i) => {
const v = (document.getElementById('obr_'+i) || {}).value || '';
html += '<label>' + p + ':</label><div class="val">' + (v.replace(/</g,'&lt;') || '—') + '</div>';
});
html += '<p style="margin-top:40px"><b>Potpis:</b> ____________________ &nbsp;&nbsp; <b>Pečat:</b></p>';
html += '</body></html>';
const w = window.open('', '_blank');
w.document.write(html); w.document.close();
setTimeout(() => w.print(), 300);
}
// Auto-attach klub picker to all invoice klub inputs
(function() {
if (window._erpKlubPickerHook) return;
window._erpKlubPickerHook = true;
const obs = new MutationObserver(() => {
if (window.state && (state.page === 'invoices' || state.page === 'expenses')) {
// Find any input with klub keyword that's not already wrapped
document.querySelectorAll('input').forEach(inp => {
if (inp._klubPickerAttached) return;
const ph = (inp.placeholder || '').toLowerCase();
const id = (inp.id || '').toLowerCase();
const name = (inp.name || '').toLowerCase();
if ((ph.includes('klub') || id.includes('klub') || name.includes('klub'))
&& !inp.closest('.klub-picker-wrap')
&& inp.type !== 'hidden') {
inp._klubPickerAttached = true;
if (!inp.id) inp.id = 'klubInp_' + Math.random().toString(36).substr(2,8);
attachKlubPicker(inp.id);
}
});
}
});
obs.observe(document.body, {childList:true, subtree:true});
})();
// Aggressively attach klub picker to invKlub input whenever it appears
(function() {
if (window._invKlubAttachHook) return;
window._invKlubAttachHook = true;
let lastSeen = null;
setInterval(() => {
const el = document.getElementById('invKlub');
if (el && el !== lastSeen && !el._klubPickerAttached) {
el._klubPickerAttached = true;
lastSeen = el;
if (typeof attachKlubPicker === 'function') {
attachKlubPicker('invKlub');
console.log('[ri] klub picker attached to invKlub');
}
}
}, 500);
})();
// ===== RNO =====
async function pageRno() {
document.getElementById('content').innerHTML = `<div class="page-h"><h2>Registar NPO</h2><p class="muted">1.505 sportskih organizacija PGZ iz Registra neprofitnih — s financijskim podacima u EUR.</p></div>
<div class="card" style="margin-bottom:12px;padding:10px 12px;background:var(--bg3)"><div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<span style="font-size:11px;color:var(--muted)">Pretraga</span>
<input id="rnoSearch" class="i" type="text" placeholder="Naziv, OIB, mjesto..." style="flex:1;min-width:160px" oninput="loadRno()">
<span style="font-size:11px;color:var(--muted)">Status</span>
<select id="rnoStatus" class="i" onchange="loadRno()" style="min-width:100px"><option value="">Sve</option><option value="active">Aktivne</option><option value="inactive">Neaktivne</option></select>
<span style="font-size:11px;color:var(--muted)">Sortiraj</span>
<select id="rnoSort" class="i" onchange="loadRno()" style="min-width:110px"><option value="naziv">Naziv</option><option value="prihodi">Prihodi</option><option value="rashodi">Rashodi</option></select>
</div></div>
<div id="rnoList" class="loader">Ucitavanje...</div>`;
loadRno();
}
async function loadRno() {
const q=document.getElementById('rnoSearch')?.value||'', status=document.getElementById('rnoStatus')?.value||'', sort=document.getElementById('rnoSort')?.value||'naziv';
const data=await api('/api/v2/rno?'+new URLSearchParams({q,status,sort,limit:100}));
const el=document.getElementById('rnoList'); if(!el)return;
if(!data?.length){el.innerHTML='<p class="muted">Nema rezultata.</p>';return;}
const ef=n=>n?new Intl.NumberFormat('hr-HR',{style:'currency',currency:'EUR',maximumFractionDigits:0}).format(n<1000&&n>0?n:n/7.5345):'-';
el.innerHTML=`<div style="font-size:11px;color:var(--muted);margin-bottom:8px">${data.length} organizacija</div><div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:12px">
<thead><tr style="background:var(--bg3)"><th style="text-align:left;padding:7px 10px;font-size:10px;color:var(--muted)">NAZIV</th><th style="padding:7px 10px;font-size:10px;color:var(--muted)">MJESTO</th><th style="text-align:right;padding:7px 10px;font-size:10px;color:var(--muted)">PRIHODI EUR</th><th style="text-align:right;padding:7px 10px;font-size:10px;color:var(--muted)">RASHODI EUR</th><th style="text-align:center;padding:7px 10px;font-size:10px;color:var(--muted)">AKT.</th></tr></thead>
<tbody>${data.map(r=>`<tr style="border-top:1px solid var(--border)"><td style="padding:6px 10px"><div style="color:var(--text);font-weight:500">${r.naziv}</div><div style="font-size:10px;color:var(--muted)">${r.oib||''}</div></td><td style="padding:6px 10px;font-size:11px;color:var(--text2)">${r.mjesto||'-'}</td><td style="padding:6px 10px;text-align:right;font-family:monospace;font-size:11px">${ef(r.prihodi)}</td><td style="padding:6px 10px;text-align:right;font-family:monospace;font-size:11px">${ef(r.rashodi)}</td><td style="padding:6px 10px;text-align:center;color:${r.aktivna?'#22c55e':'var(--muted)'}">${r.aktivna?'✓':'-'}</td></tr>`).join('')}</tbody>
</table></div>`;}
// ===== HNS =====
async function pageHns() {
document.getElementById('content').innerHTML=`<div class="page-h"><h2>HNS Natjecanja PGZ</h2><p class="muted">NS Rijeka i ZNS PGZ — sezone 2022-2026.</p></div>
<div class="card" style="margin-bottom:12px;padding:10px 12px;background:var(--bg3)"><div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<span style="font-size:11px;color:var(--muted)">Sezona</span>
<select id="hnsSeason" class="i" onchange="loadHns()" style="min-width:110px"><option value="">Sve</option><option value="2025/2026" selected>2025/2026</option><option value="2024/2025">2024/2025</option><option value="2023/2024">2023/2024</option><option value="2022/2023">2022/2023</option></select>
<span style="font-size:11px;color:var(--muted)">Org.</span>
<select id="hnsOrg" class="i" onchange="loadHns()" style="min-width:120px"><option value="">Obje</option><option value="178180">NS Rijeka</option><option value="51">ZNS PGZ</option></select>
</div></div>
<div id="hnsList" class="loader">Ucitavanje...</div>`;
loadHns();
}
async function loadHns(){
const season=document.getElementById('hnsSeason')?.value||'', org=document.getElementById('hnsOrg')?.value||'';
const data=await api('/api/v2/hns-natjecanja?'+new URLSearchParams({season,org}));
const el=document.getElementById('hnsList'); if(!el)return;
if(!data?.length){el.innerHTML='<p class="muted">Nema natjecanja.</p>';return;}
const bySeason={};
data.forEach(r=>{if(!bySeason[r.sezona])bySeason[r.sezona]=[];bySeason[r.sezona].push(r);});
el.innerHTML=Object.entries(bySeason).sort((a,b)=>b[0].localeCompare(a[0])).map(([s,comps])=>`
<div style="margin-bottom:16px"><div style="font-size:10px;font-weight:700;color:var(--accent);margin-bottom:8px;text-transform:uppercase;letter-spacing:1px">Sezona ${s} - ${comps.length} natjecanja</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:8px">
${comps.map(c=>`<a href="${c.url||'#'}" target="_blank" style="display:block;padding:10px 12px;background:var(--bg3);border:1px solid var(--border);border-radius:6px;text-decoration:none;font-size:12px;font-weight:500;color:var(--text);transition:border-color .15s" onmouseover="this.style.borderColor='var(--accent)'" onmouseout="this.style.borderColor='var(--border)'">${c.naziv}<span style="display:block;font-size:10px;color:var(--muted);margin-top:2px;font-weight:400">${c.org_id==178180?'NS Rijeka':'ZNS PGZ'}</span></a>`).join('')}
</div></div>`).join('');}
// ===== GODISNJACI =====
async function pageGodisnjaci(){
document.getElementById('content').innerHTML=`<div class="page-h"><h2>Godisnjaci ZSP PGZ — AI</h2><p class="muted">AI pretraga 19 godisnjaka (2006-2024). Pitaj na prirodnom jeziku.</p></div>
<div class="card" style="margin-bottom:12px;padding:10px 12px;background:var(--bg3)">
<div style="display:flex;gap:8px"><input id="godQ" class="i" type="text" placeholder="Npr: Koliko kosarkaskih klubova 2015?" style="flex:1"><button class="btn" onclick="searchGod()">Pretrazi</button></div>
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px" id="godChips"></div></div>
<div id="godLoader" style="display:none" class="loader">AI pretrazuje...</div>
<div id="godResult" style="display:none"><div id="godAns" class="card" style="margin-bottom:10px;background:var(--bg4)"></div><div id="godSrc"></div></div>`;
const chips=['Kosarka Rijeka','Proracun sport 2019','Planinarstvo PGZ','Atletika mladi','Vaterpolo'];
document.getElementById('godChips').innerHTML=chips.map(q=>`<span onclick="document.getElementById('godQ').value='${q}';searchGod()" style="cursor:pointer;padding:2px 10px;background:var(--bg3);border:1px solid var(--border);border-radius:12px;font-size:11px;color:var(--text2)">${q}</span>`).join('');
document.getElementById('godQ').addEventListener('keydown',e=>{if(e.key==='Enter')searchGod();});
}
async function searchGod(){
const q=document.getElementById('godQ').value.trim(); if(!q)return;
document.getElementById('godLoader').style.display='block';
document.getElementById('godResult').style.display='none';
try{
const res=await api('/api/v2/godisnjaci/search',{method:'POST',body:JSON.stringify({question:q})});
document.getElementById('godLoader').style.display='none';
document.getElementById('godResult').style.display='block';
document.getElementById('godAns').innerHTML=`<div style="font-size:10px;color:var(--accent);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px">AI Odgovor</div><div style="font-size:13px;line-height:1.6">${res.answer||'Nema odgovora.'}</div>`;
document.getElementById('godSrc').innerHTML=(res.sources||[]).map(s=>`<div class="card" style="margin-bottom:8px;font-size:12px"><div style="color:var(--accent);margin-bottom:4px">Godisnjak ${s.godina}</div><div style="color:var(--text2);line-height:1.5">${(s.text||'').slice(0,300)}...</div></div>`).join('');
}catch(e){document.getElementById('godLoader').style.display='none';document.getElementById('godResult').style.display='block';document.getElementById('godAns').innerHTML=`<span class="dim">Greska: ${e.message}</span>`;}}
// Browser back/forward button support
window.addEventListener('popstate', function() {
const h = window.location.hash.slice(1);
if (h && typeof window['page'+h[0].toUpperCase()+h.slice(1)] === 'function') {
window['page'+h[0].toUpperCase()+h.slice(1)]();
} else if (h) {
goto(h);
}
});
// ===== SPORTSKI OBJEKTI =====
async function pageObjekti() {
document.getElementById('content').innerHTML = `<div class="page-h"><h2>Sportski objekti PGZ</h2><p class="muted">106 sportskih objekata — dvorane, stadioni, bazeni, tereni. Klik otvara na karti.</p></div>
<div class="card" style="margin-bottom:12px;padding:10px 12px;background:var(--bg3)"><div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<span style="font-size:11px;color:var(--muted)">Tip</span>
<select id="objTip" class="i" onchange="loadObjekti()" style="min-width:120px">
<option value="">Svi tipovi</option>
<option value="dvorana">Dvorana</option>
<option value="stadion">Stadion</option>
<option value="bazen">Bazen</option>
<option value="teren">Teren</option>
<option value="kompleks">Kompleks</option>
<option value="klizaliste">Klizaliste</option>
</select>
<span style="font-size:11px;color:var(--muted)">Grad</span>
<select id="objGrad" class="i" onchange="loadObjekti()" style="min-width:110px">
<option value="">Svi gradovi</option>
</select>
<input id="objQ" class="i" type="text" placeholder="Pretrazi..." style="flex:1;min-width:120px" oninput="loadObjekti()">
</div></div>
<div id="objList" class="loader">Ucitavanje...</div>`;
loadObjekti();
}
async function loadObjekti() {
const tip=document.getElementById('objTip')?.value||'', grad=document.getElementById('objGrad')?.value||'', q=document.getElementById('objQ')?.value||'';
const data = await api('/api/v2/sport/objekti?'+new URLSearchParams({tip,grad,q}));
const el=document.getElementById('objList'); if(!el)return;
// Populate grad dropdown on first load
if(!document.getElementById('objGrad').options.length || document.getElementById('objGrad').options.length===1) {
const grads=[...new Set((data||[]).map(r=>r.grad).filter(Boolean))].sort();
const gEl=document.getElementById('objGrad');
gEl.innerHTML='<option value="">Svi gradovi</option>'+grads.map(g=>`<option value="${g}">${g}</option>`).join('');
}
if(!data?.length){el.innerHTML='<p class="muted" style="padding:20px">Nema objekata za filtere.</p>';return;}
el.innerHTML=`<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px">
${data.filter(r=>!q||(r.naziv||'').toLowerCase().includes(q.toLowerCase())).map(r=>`
<div class="card" style="padding:12px 14px;background:var(--bg3)">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
<span style="font-size:12px;font-weight:600;color:var(--text)">${r.naziv||'—'}</span>
<span style="font-size:10px;padding:2px 6px;background:rgba(var(--accent-rgb,245,158,11),.15);color:var(--accent);border-radius:10px;text-transform:capitalize">${r.tip||'—'}</span>
</div>
<div style="font-size:11px;color:var(--text2);margin-bottom:4px">📍 ${r.adresa||r.grad||'—'}</div>
${r.sportovi?.length?`<div style="font-size:10px;color:var(--muted);margin-bottom:4px">⚽ ${r.sportovi.join(', ')}</div>`:''}
${r.kapacitet?`<div style="font-size:10px;color:var(--muted)">👥 ${r.kapacitet} mjesta · Izgradeno: ${r.izgradeno||'?'}</div>`:''}
${r.web?`<a href="${r.web}" target="_blank" style="font-size:10px;color:var(--accent);text-decoration:none">🔗 Web</a>`:''}
${r.lat&&r.lng?`<a href="https://maps.google.com/?q=${r.lat},${r.lng}" target="_blank" style="font-size:10px;color:var(--accent);margin-left:8px;text-decoration:none">🗺 Karta</a>`:''}
</div>`).join('')}
</div>`;}
</script>
</body>
</html>