Files
pgz-sport/static/index.html.bak_final
T

5263 lines
284 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="hr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, 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>
</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>
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', items:[
{ id:'dashboard', label:'Dashboard', mlabel:'Home', 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:'search', label:'AI Search', svg:'<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>' },
{ id:'analytics', label:'Analytics', svg:'<polyline points="3,17 9,11 13,15 21,7"/><polyline points="14,7 21,7 21,14"/>' },
{ id:'alertovi', label:'Alertovi', mlabel:'Alerti', badge:'alerts', 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"/>' }
]},
{ sec:'Organizacija', items:[
{ id:'savezi', label:'Savezi', 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:'klubovi', label:'Klubovi', svg:'<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/>' },
{ id:'baza', label:'PGŽ baza', mlabel:'PGŽ baza', svg:'<rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/>' },
{ id:'dokumenti', label:'Pravilnici i zakoni', mlabel:'Pravilnici', 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"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>' },
{ id:'kategorije', label:'Dobne kategorije', mlabel:'Kategorije', 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:'funkcionari', label:'Funkcionari', mlabel:'Funkc', svg:'<path d="M16 21v-2a4 4 0 0 0-3-4H8a4 4 0 0 0-4 4v2"/><circle cx="10" cy="7" r="3"/><path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/>' },
{ id:'sportasi', label:'Igrači · Foto', mlabel:'Igrači', svg:'<circle cx="12" cy="7" r="4"/><path d="M5 21v-2a4 4 0 0 1 4-4h6a4 4 0 0 1 4 4v2"/>' },
{ id:'audit', label:'Audit · Kvaliteta', mlabel:'Audit', svg:'<path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>', roles:['super_admin','pgz_admin','pgz_user'] },
{ id:'clanovi', label:'Članovi', mlabel:'Članovi', 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"/>' }
]},
{ sec:'Financije', items:[
{ id:'clanarine', label:'Članarine', mlabel:'Plaćanja', 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', svg:'<polygon points="12,2 15.09,8.26 22,9.27 17,14.14 18.18,21.02 12,17.77 5.82,21.02 7,14.14 2,9.27 8.91,8.26"/>' },
{ id:'proracun', label:'Proračun PGŽ', mlabel:'Proračun', svg:'<line x1="12" y1="20" x2="12" y2="10"/><line x1="18" y1="20" x2="18" y2="4"/><line x1="6" y1="20" x2="6" y2="16"/>' }
]},
{ sec:'Zdravlje', items:[
{ id:'lijecnicki', label:'Liječnički', svg:'<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>' },
{ id:'zzjz', label:'ZZJZ PGŽ', mlabel:'ZZJZ', svg:'<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>' }
]},
{ sec:'Operativa', items:[
{ id:'manifestacije', label:'Manifestacije', mlabel:'Eventi', svg:'<path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/>' },
{ id:'sportStats', label:'⚽ Sport Stats', mlabel:'Stats', mlabel:'Stats', 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:'ERP & Pravo', items:[
{ id:'ask', label:'AI Asistent', mlabel:'AI', svg:'<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/>' },
{ id:'invoices', label:'Računi (ERP)', mlabel:'Računi', 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\"/><line x1=\"16\" y1=\"13\" x2=\"8\" y2=\"13\"/><line x1=\"16\" y1=\"17\" x2=\"8\" y2=\"17\"/>' },
{ id:'expenses', label:'Putni nalozi', mlabel:'Putni', 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\"/>' },
{ id:'forms', label:'Obrasci', mlabel:'Obrasci', 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:'users', label:'Korisnici', mlabel:'Users', 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\"/>' }
]}];
// Bottom mob nav (only top 5)
const MOB_NAV = ['dashboard','klubovi','clanovi','lijecnicki','alertovi'];
function buildNavs() {
// Desktop nav
const dEl = document.getElementById('nav-desktop');
const mEl = document.getElementById('nav-mob');
let dHtml = '', mHtml = '';
NAV.forEach(s => {
dHtml += `<div class="nav-sec">${s.sec}</div>`;
mHtml += `<div class="nav-sec">${s.sec}</div>`;
s.items.forEach(it => {
const badge = it.badge ? `<span class="b" id="b-${it.id}" style="display:none">0</span>` : '';
const item = `<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>`;
dHtml += item;
mHtml += item.replace(`b-${it.id}`, `b-${it.id}-m`);
});
});
dEl.innerHTML = dHtml;
mEl.innerHTML = mHtml;
// Mobile bottom nav (5 max)
const bn = document.getElementById('mob-nav');
let bHtml = '<div class="mob-nav-grid">';
MOB_NAV.forEach(id => {
const it = NAV.flatMap(s => s.items).find(x => x.id===id);
if (!it) return;
bHtml += `<div class="mob-nav-i" data-page="${id}" onclick="goto('${id}');toggleMobDrawer(false)">
<svg class="ico ico-svg" viewBox="0 0 24 24">${it.svg}</svg>
<span>${it.mlabel||it.label}</span>
</div>`;
});
// 5th = MORE
bHtml += `<div class="mob-nav-i" onclick="toggleMobDrawer(true)">
<svg class="ico ico-svg" viewBox="0 0 24 24"><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>
<span>Više</span>
</div></div>`;
bn.innerHTML = bHtml;
}
function goto(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 || '') + '"');
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)">🎤</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)">🎤</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)">🎤</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 = 'navigate(\x27savezi\x27)';
else { var sq = q.replace(/\x27/g, ''); onClick = 'state.searchQ=\x27' + sq + '\x27; navigate(\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');
navigate('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() {
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>`; }
}
async function pageAnalytics() {
setTopbar('Analytics', 'Detaljne analize');
const c = document.getElementById('content');
const metric = state.filters.metric || 'registriranih';
const godine = state.filters.godine || '2020,2021,2022,2023,2024';
try {
const [data, proracun] = await Promise.all([
api(`/api/analytics/savezi-trend?metric=${metric}&godine=${godine}`),
api('/api/analytics/proracun-detaljno')
]);
const allG = data.godine;
const series = Object.entries(data.data).map(([n,v]) => ({label:n, data:allG.map(g=>parseInt(v[g]||0))}));
series.sort((a,b)=>b.data.reduce((x,y)=>x+y,0) - a.data.reduce((x,y)=>x+y,0));
const top = series.slice(0,8);
const ML = {registriranih:'Registrirani', neregistriranih:'Neregistr.', rekreativaca:'Rekreativci', trenera:'Treneri', reprezentativaca:'Reprezent.', kategoriziranih:'Kategoriz.', stipendiranih:'Stipend.', klubova_clanica:'Klubovi-čl.'};
c.innerHTML = `
<div class="fbar">
<div class="fbar-t">⚙ Parametri analize</div>
<div class="fgrid">
<div class="fitem"><label>Metrika</label>
<select onchange="state.filters.metric=this.value;render()">
${Object.entries(ML).map(([k,v])=>`<option value="${k}" ${metric==k?'selected':''}>${v}</option>`).join('')}
</select>
</div>
<div class="fitem"><label>Godine (CSV)</label>
<input class="inp mono" value="${godine}" onchange="state.filters.godine=this.value;render()">
</div>
<div class="fitem"><label>&nbsp;</label>
<button class="btn sec" onclick="state.filters={};render()">↻ Reset</button>
</div>
</div>
</div>
<div class="sect">Trend · ${ML[metric]}</div>
<div class="card" style="margin-bottom:14px">
<div class="ct">📊 Top 8 saveza · ${allG[0]} — ${allG[allG.length-1]}.</div>
${top.length?lineChart(top, allG, 1200, 280):'<div class="empty">Bez podataka</div>'}
</div>
<div class="sect">Tabelarni prikaz</div>
<div class="tbl-wrap">
<table>
<thead><tr><th>Savez</th>${allG.map(g=>`<th style="text-align:right">${g}.</th>`).join('')}<th style="text-align:right">Σ</th><th style="text-align:right">Trend</th></tr></thead>
<tbody>${Object.entries(data.data).sort((a,b)=>Object.values(b[1]).reduce((x,y)=>x+(y||0),0)-Object.values(a[1]).reduce((x,y)=>x+(y||0),0)).map(([n,v])=>{
const arr = allG.map(g=>v[g]||0);
const sum = arr.reduce((a,b)=>a+b,0);
const tr = arr[arr.length-1]-arr[0];
const trC = tr>0?'var(--ok)':tr<0?'var(--crit)':'var(--text-3)';
return `<tr><td><b>${n}</b></td>${arr.map(x=>`<td class="num">${fmt(x)}</td>`).join('')}<td class="num"><b>${fmt(sum)}</b></td><td class="num" style="color:${trC};font-weight:600">${tr>0?'↗ +':''}${tr}</td></tr>`;
}).join('')}</tbody>
</table>
</div>
<div class="sect">Proračun PGŽ · detaljno</div>
<div class="grid g3">
<div class="card ok"><div class="stat-l">${proracun.current_year}.</div><div class="stat-v sm">${fmtEur(proracun.current_total)}</div></div>
<div class="card acc"><div class="stat-l">Rast 10g</div><div class="stat-v">${proracun.rast_dekada_pct}%</div></div>
<div class="card"><div class="stat-l">Godina podataka</div><div class="stat-v">${proracun.proracun?.length||0}</div></div>
</div>
<div class="card" style="margin-top:12px">
<div class="ct">📈 Godišnji rast YoY</div>
${(proracun.rast_godisnji||[]).map(r=>{
const cl = r.rast_postotak>0?'ok':r.rast_postotak<0?'crit':'';
return `<div class="bar"><div class="l">${r.godina}.</div><div class="t"><div class="f ${cl}" style="width:${Math.min(Math.abs(r.rast_postotak)*1.5,100).toFixed(1)}%"></div></div><div class="v">${r.rast_postotak>0?'+':''}${r.rast_postotak}%</div></div>`;
}).join('')}
</div>
`;
} catch (e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
}
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=>`<tr><td>${k.naziv}</td><td><span class="bdg muted">${k.razina||''}</span></td><td class="dim" style="text-align:right">${k.grad||''}</td></tr>`).join('')}</tbody></table>`:''}
</div>`);
} catch(e) { alert(e.message); }
}
async function pageKlubovi() {
setTopbar('Organizacija', 'Klubovi');
const c = document.getElementById('content');
const q = document.getElementById('q-klub')?.value||'';
const fnos = state.filters.nositelj||'';
const freg = state.filters.region||'';
try {
const d = await api('/api/klubovi?'+(q?`q=${encodeURIComponent(q)}`:'')+(fnos?`&nositelj=${fnos}`:'')+(freg?`&region=${freg}`:'')+getSort('klubovi'));
c.innerHTML = `
<div class="toolbar">
<input class="inp flex" id="q-klub" placeholder="🔍 Pretraga..." value="${q}" oninput="dbK()">
<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:'predsjednik',label:'Predsjednik',sort:false},{key:'oib',label:'OIB',sort:false},{key:'savez',label:'Savez'},{key:'broj_clanova',label:'Čl.'},{key:'enrich',label:'Status',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>`; }
}
const dbK = debounce(pageKlubovi, 300);
async function showKlub(id) {
try {
const d = await api('/api/klubovi/' + id);
const initial = (d.naziv||'?').replace(/[^A-Za-zŠŽČĆĐšžčćđ]/g,'').slice(0,2).toUpperCase() || '?';
const stats = d.stats || {};
const clanoviHTML = (d.clanovi||[]).map(c => {
const ini = ((c.ime||'?')[0]+(c.prezime||'?')[0]).toUpperCase();
const flags = [];
if (c.reprezentativac) flags.push('<span class="clan-flag" style="background:rgba(245,158,11,0.15);color:var(--gold)">🇭🇷 REPR.</span>');
if (c.kategoriziran) flags.push('<span class="clan-flag" style="background:rgba(59,130,196,0.15);color:var(--accent)">⭐ KAT.</span>');
return `<div class="clan-card">
<div class="clan-head">
<div class="clan-avatar">${ini}</div>
<div class="clan-name-x"><div class="nm">${c.prezime} ${c.ime}</div><div class="pos">${c.pozicija || c.kategorija || ''}</div></div>
</div>
<div class="clan-flags">${flags.join('')}</div>
<div style="font-size:10px;color:var(--text-dim);margin-top:5px">${c.spol||''} · rođ. ${fmtDate(c.datum_rodenja)}</div>
<div class="mono" style="font-size:10px;color:var(--text-dim);margin-top:3px">OIB: ${c.oib||''}</div>
</div>`;
}).join('');
openDrawer(`
<div class="drawer-head">
<div style="display:flex;gap:12px;align-items:flex-start;flex:1">
<div class="klub-logo" style="width:48px;height:48px;font-size:16px">${initial}</div>
<div>
<div class="breadcrumb">KLUB · #${d.id}</div>
<h3>${d.naziv}</h3>
<div style="margin-top:6px;display:flex;gap:5px;flex-wrap:wrap">
${d.sport ? `<span class="badge muted">${d.sport}</span>` : ''}
${d.razina ? `<span class="badge info">${d.razina}</span>` : ''}
${d.nositelj_kvalitete ? '<span class="badge gold">⭐ Nositelj kvalitete</span>' : ''}
</div>
</div>
</div>
<button class="drawer-close" onclick="closeDrawer()">✕</button>
</div>
<div class="drawer-body">
<div class="drawer-stats-grid">
<div class="drawer-stat accent"><div class="v">${stats.broj_clanova||0}</div><div class="l">Članova</div></div>
<div class="drawer-stat"><div class="v">${stats.broj_registriranih||0}</div><div class="l">Reg.</div></div>
<div class="drawer-stat"><div class="v">${stats.broj_trenera||0}</div><div class="l">Treneri</div></div>
<div class="drawer-stat ok"><div class="v">${stats.broj_reprezentativaca||0}</div><div class="l">Reprez.</div></div>
</div>
<div class="drawer-stats-grid">
<div class="drawer-stat ok"><div class="v">${stats.lijecnicki_validni||0}</div><div class="l">Lij. ✓</div></div>
<div class="drawer-stat warn"><div class="v">${stats.lijecnicki_uskoro||0}</div><div class="l">Lij. ~</div></div>
<div class="drawer-stat crit"><div class="v">${stats.lijecnicki_istekli||0}</div><div class="l">Lij. ✗</div></div>
<div class="drawer-stat ok"><div class="v" style="font-size:13px">${fmtEur(stats.potpore_2025).replace(/\s.*/,'')}</div><div class="l">Potpore</div></div>
</div>
<h4>Osnovni podaci</h4>
<dl>
<dt>OIB ${!state.isAdmin?'<span class="blur-tag">priv.</span>':''}</dt><dd class="mono">${d.oib || ''}</dd>
<dt>Adresa</dt><dd>${d.adresa ? d.adresa + ', ' : ''}${d.grad || ''}${d.region ? ` (${d.region})` : ''}</dd>
<dt>Predsjednik</dt><dd>${d.predsjednik || ''}</dd>
<dt>Tajnik</dt><dd>${d.tajnik || ''}</dd>
<dt>Glavni trener</dt><dd>${d.trener_glavni || ''}</dd>
<dt>Email</dt><dd>${d.email || ''}</dd>
<dt>Telefon</dt><dd>${d.telefon || ''}</dd>
<dt>IBAN</dt><dd class="mono">${d.iban || ''}</dd>
<dt>Web</dt><dd>${d.web ? `<a href="${d.web}" target="_blank">${d.web}</a>` : ''}</dd>
<dt>Osnovan</dt><dd>${d.godina_osnutka || (d.datum_osnivanja_full ? d.datum_osnivanja_full.substring(0,10) : '')}</dd>
${d.reg_broj ? `<dt>Reg. broj</dt><dd class="mono">${d.reg_broj}</dd>` : ''}
${d.udruga_status ? `<dt>Status u registru</dt><dd><span class="badge ${d.udruga_status==='AKTIVAN'?'ok':'warn'}">${d.udruga_status}</span></dd>` : ''}
</dl>
${d.sjediste ? `<h4>📍 Sjedište (FINA registar)</h4>
<div class="banner info" style="font-size:13px">${d.sjediste}</div>` : ''}
${(d.ciljevi || d.opis_djelatnosti) ? `<h4>🎯 Ciljevi i djelatnost</h4>
<div style="background:rgba(59,130,196,0.05);padding:12px;border-left:3px solid var(--accent);font-size:12px;line-height:1.5">
${d.ciljevi ? `<div style="color:var(--text)"><strong>Ciljevi:</strong> ${d.ciljevi}</div>` : ''}
${d.opis_djelatnosti ? `<div style="color:var(--text-dim);margin-top:8px"><strong>Djelatnosti:</strong> ${d.opis_djelatnosti}</div>` : ''}
</div>` : ''}
${d.web_stranica ? `<h4>🌐 Web</h4>
<div><a href="${d.web_stranica}" target="_blank" style="color:var(--accent)">${d.web_stranica}</a></div>` : ''}
${d.napomena ? `<div class="banner info" style="margin-top:12px;font-size:12px">${d.napomena}</div>` : ''}
${(d.potpore||[]).length ? `<h4>💰 Potpore PGŽ</h4>
<table class="subtable">
<thead><tr><th>Godina</th><th style="text-align:right">Iznos</th></tr></thead>
<tbody>${d.potpore.map(p => `<tr><td><strong>${p.godina}</strong></td><td class="num">${fmtEur(p.iznos)}</td></tr>`).join('')}</tbody>
</table>` : ''}
${(d.clanovi||[]).length ? `<h4>👥 Članovi (${d.clanovi.length})</h4>
<div class="clan-list">${clanoviHTML}</div>` : '<h4>👥 Članovi (0)</h4><div class="empty" style="padding:20px">Bez članova</div>'}
${(d.lijecnicki||[]).length ? `<h4>🏥 Liječnički pregledi (${d.lijecnicki.length})</h4>
<table class="subtable">
<thead><tr><th>Sportaš</th><th>Datum</th><th>Vrijedi do</th><th>Status</th></tr></thead>
<tbody>${d.lijecnicki.slice(0,15).map(l => `<tr>
<td>${l.clan}</td><td class="dim">${fmtDate(l.datum_pregleda)}</td>
<td>${fmtDate(l.vrijedi_do)}</td>
<td><span class="badge ${l.status_pregled==='Validan'?'ok':l.status_pregled==='Ističe uskoro'?'warn':'crit'}">${l.status_pregled}</span></td>
</tr>`).join('')}</tbody></table>` : ''}
${(d.clanarine||[]).length ? `<h4>💳 Članarine (${d.clanarine.length})</h4>
<table class="subtable">
<thead><tr><th>God.</th><th>Član</th><th style="text-align:right">Propis.</th><th style="text-align:right">Plać.</th><th>Status</th></tr></thead>
<tbody>${d.clanarine.slice(0,20).map(cl => `<tr>
<td>${cl.godina}</td><td>${cl.clan}</td>
<td class="num">${fmtEur(cl.iznos_propisan)}</td><td class="num" style="color:var(--ok)">${fmtEur(cl.iznos_placen)}</td>
<td><span class="badge ${cl.status==='podmireno'?'ok':cl.status==='djelomicno'?'warn':'crit'}">${cl.status}</span></td>
</tr>`).join('')}</tbody></table>` : ''}
</div>
`);
} catch (e) { alert('Greška: '+e.message); }
}
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>',
'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()] || '🏃'; }
const routes = { 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 };
// ===== 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 = '🎤';
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 = '🎤'; }
};
rec.onend = function() {
if (btnEl) { btnEl.classList.remove('recording'); btnEl.innerHTML = '🎤'; }
// 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 = '🎤'; }
}
}
// ===== 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 body = (m.content||'').replace(/</g,'&lt;');
let metaHtml = '';
if (m.llm) metaHtml = '<div class="v6-msg-meta">🤖 ' + m.llm + (m.hits ? ' · '+m.hits+' izvora' : '') + '</div>';
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>';
}
return '<div class="v6-chat-msg bot">' + metaHtml + body + srcHtml + '</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 — synthesize a list-style answer
if (!d.results || !d.results.length) {
chatHistory.push({role:'bot', content:'Nisam pronašao ništa relevantno za "'+q+'". Probaj preformulirati.'});
} else {
const top3 = d.results.slice(0, 5);
let answer = 'Evo šta sam pronašao u bazi (' + d.results.length + ' rezultata):\n\n';
top3.forEach(function(h, i){
const title = h.title || (h.payload && h.payload.title) || '?';
const snippet = (h.snippet || '').slice(0, 200);
answer += (i+1) + '. **' + title + '** (' + (h.score*100).toFixed(0) + '%)';
if (snippet) answer += '\n ' + snippet + (snippet.length>=200?'…':'');
answer += '\n\n';
});
chatHistory.push({role:'bot', content: answer.trim(), sources: top3, 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 (🎤) ili tekstom.</div>';
}
function render() {
const fn = routes[state.page] || pageDashboard;
fn().catch(e => document.getElementById('content').innerHTML = `<div class="ban crit"><b>Greška:</b> ${e.message}</div>`);
}
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 🎤 (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)">🎤</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 (🎤) 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)">🎤</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;
const klub_id = parseInt(document.getElementById('invKlub').value || '0');
if (!f) { alert('Odaberi datoteku'); return; }
if (!klub_id) { alert('Unesi klub_id'); 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) { alert('Detail UI TODO — ID '+id); }
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"><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"><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" 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" 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>${doc.izvor_url ? `<a href="${doc.izvor_url}" target="_blank" class="btn" style="padding:2px 8px;font-size:10px">↗ Izvor</a>`: ''}</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" 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>Web</th></tr></thead><tbody>';
for (const o of grouped[grad]) {
html += `<tr><td><b>${o.naziv}</b></td>
<td><span style="display:inline-block;padding:2px 6px;background:var(--bg4);border-radius:3px;font-size:10px">${o.tip}</span></td>
<td class="muted" style="font-size:11px">${o.adresa||'-'}</td>
<td class="muted" style="font-size:11px">${o.upravitelj||'-'}</td>
<td class="mono">${o.kapacitet ? Number(o.kapacitet).toLocaleString() : '-'}</td>
<td class="muted" style="font-size:10px">${(o.sportovi||[]).join(', ')}</td>
<td class="mono">${o.izgradeno||'-'}</td>
<td>${o.web ? `<a href="${o.web}" target="_blank" class="muted">↗</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" 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)) {
html += `<tr><td><b>${n.naziv}</b></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" 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></tr></thead><tbody>';
for (const m of d.results) {
html += `<tr><td><b>${m.naziv}</b></td>
<td>${m.mjesto||'-'}</td>
<td class="muted" style="font-size:11px">${m.organizator||'-'}</td>
<td><span style="display:inline-block;padding:2px 6px;background:var(--bg4);border-radius:3px;font-size:10px">${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">${m.savez_naziv||'-'}</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" 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]) {
html += `<tr><td><b>${n.kategorija}</b></td>
<td>${n.ime_prezime||'-'}</td>
<td class="muted">${n.klub||'-'}</td>
<td class="muted" style="font-size:11px">${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" 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) {
html += `<tr><td class="mono">${p.godina}</td>
<td><b>${p.naziv_kluba}</b></td>
<td class="muted" style="font-size:11px">${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" 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) {
html += `<tr><td><b>${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"><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]) {
h += `<tr>
<td><b>${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.sport||'-'}</span></td>
<td>${x.klub_naziv||'-'}</td>
<td class="muted">${x.mjesto_rodenja||'-'}</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"><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"><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]) {
h += `<tr><td><b>${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">${x.organizacija||'-'}</td>
<td class="muted">${x.grad||'-'}</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"><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) {
h += `<tr><td><span style="display:inline-block;padding:2px 6px;background:var(--bg4);border-radius:3px;font-size:10px">${x.sport}</span></td>
<td><b>${x.ime} ${x.prezime||''}</b></td>
<td>${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||'-'}</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"><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]) {
h += `<tr><td><b>${x.sponzor}</b></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"><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]) {
h += `<tr><td><b>${x.naziv}</b></td>
<td>${x.grad||'-'}</td>
<td class="muted" style="font-size:11px">${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"><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) {
h += `<tr><td><b>${x.naziv}</b></td>
<td class="muted">${x.fakultet||'-'}</td>
<td>${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"><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],
['Sportaša', st.broj_sportasa],
['Kategoriziranih', st.broj_kategoriziranih],
['Reprezentativaca', st.broj_reprezentativaca],
['Saveza', d.savezi.length],
['Manifestacija', d.manifestacije.length],
].map(([lbl,v]) => `<div style="text-align:center;padding:6px">
<div class="ri-kpi-value" style="font-size:20px">${(v||0).toLocaleString('hr-HR')}</div>
<div class="ri-kpi-label">${lbl}</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"><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"><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="${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||'igrac')}</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"><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"><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"><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 => {
html += `<tr>
<td><b>${m.naziv}</b></td>
<td>${m.mjesto||'-'}</td>
<td>${m.razina||'-'}</td>
<td class="mono">${m.godina_od||'-'}</td>
<td>${m.broj_ucesnika||'-'}</td>
<td class="muted" style="font-size:11px">${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 d = await fetch('/sport/api/klubovi').then(r=>r.json());
const sel = document.getElementById('spKlubFilter');
if (!sel) return;
const klubovi = d.results || d.klubovi || d || [];
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('');
} 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="${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');
// Map novi format → stari (bez ponavljanja koda)
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 klubLogo = sp.logo_url || (sp.hns_klub_id ? `https://hns.family/files/images_comet/Club/_resized/${sp.hns_klub_id}_x_100_100_wg_t.png` : '');
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));
// Source warning ako nedostaje
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="${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})" ondblclick="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>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:0;border-top:1px solid var(--border)">
<div style="padding:12px;text-align:center;border-right:1px solid var(--border)"><div class="ri-kpi-value">${d.totals.nastupa||0}</div><div class="ri-kpi-label">Nastupi</div></div>
<div style="padding:12px;text-align:center;border-right:1px solid var(--border)"><div class="ri-kpi-value">${d.totals.pogodaka||0}</div><div class="ri-kpi-label">Pogoci</div></div>
<div style="padding:12px;text-align:center;border-right:1px solid var(--border)"><div class="ri-kpi-value" style="color:var(--amber)">${d.totals.zutih||0}</div><div class="ri-kpi-label">Žuti</div></div>
<div style="padding:12px;text-align:center"><div class="ri-kpi-value" style="color:var(--red)">${d.totals.crvenih||0}</div><div class="ri-kpi-label">Crveni</div></div>
</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"><thead><tr><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></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><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></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"><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})" ondblclick="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) {
html += `<div class="card"><h3 style="margin-bottom:10px">Posljednje utakmice (${d.matches.length})</h3><table class="ri-tbl"><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>`;
});
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>`;
}
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"><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="${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||'igrac')}</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>';
}
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 pageAudit() {
const c = document.getElementById('content');
setTopbar('Audit', 'Kvaliteta podataka i izvori');
c.innerHTML = '<div class="card" style="padding:24px">Učitavam audit...</div>';
try {
const d = await api('/api/v2/audit/data-quality');
let html = '';
html += breadcrumbs([{label: '🛡️ Audit kvalitete podataka'}]);
// Policy banner
html += `<div class="card" style="margin-bottom:14px;background:rgba(34,197,94,0.05);border:1px solid rgba(34,197,94,0.3)">
<h3 style="margin-bottom:8px;color:var(--green)">✓ Policy aktivna</h3>
<div style="font-size:12px;color:var(--text2)">
<div><b>Datum rođenja</b> i <b>slika</b> ne mogu biti upisani bez <code>source_url</code> — DB trigger automatski postavlja NULL ako nedostaje.</div>
<div style="margin-top:6px">Trusted sources: <code>hns_semafor</code>, <code>hbs_savez</code></div>
<div>Treba provjeru: <code>manual</code></div>
</div>
</div>`;
// Sportaši po izvoru
html += `<div class="card" style="margin-bottom:14px"><h3 style="margin-bottom:10px">Sportaši — kvaliteta po izvoru</h3>
<table class="ri-tbl"><thead><tr>
<th>Izvor</th><th style="text-align:right">Total</th>
<th style="text-align:right">Sa source_url</th>
<th style="text-align:right">Sa datumom</th>
<th style="text-align:right">Sa godinom</th>
<th style="text-align:right">Sa mjestom</th>
<th style="text-align:right">Sa slikom</th>
<th>Trust</th>
</tr></thead><tbody>`;
d.sportasi_po_izvoru.forEach(r => {
const trusted = d.trusted_sources.includes(r.source);
const pct = r.total ? Math.round(100*r.sa_izvorom/r.total) : 0;
html += `<tr>
<td><b>${r.source||'NULL'}</b></td>
<td style="text-align:right">${r.total}</td>
<td style="text-align:right;color:${pct>90?'var(--green)':pct>50?'var(--amber)':'var(--red)'}">${r.sa_izvorom} (${pct}%)</td>
<td style="text-align:right">${r.sa_dat_rod}</td>
<td style="text-align:right">${r.sa_god_rod||0}</td>
<td style="text-align:right">${r.sa_mjesto}</td>
<td style="text-align:right">${r.sa_slikom}</td>
<td>${trusted ? '<span class="risk-low">TRUSTED</span>' : '<span class="risk-medium">VERIFY</span>'}</td>
</tr>`;
});
html += '</tbody></table></div>';
// Klubovi po izvoru
html += `<div class="card" style="margin-bottom:14px"><h3 style="margin-bottom:10px">Klubovi — kvaliteta po izvoru</h3>
<table class="ri-tbl"><thead><tr>
<th>Izvor</th><th style="text-align:right">Total</th>
<th style="text-align:right">Sa scrape_url</th>
<th style="text-align:right">Sa godinom</th>
<th style="text-align:right">Sa adresom</th>
<th style="text-align:right">Sa telefonom</th>
<th style="text-align:right">Sa HNS</th>
</tr></thead><tbody>`;
d.klubovi_po_izvoru.forEach(r => {
html += `<tr>
<td><b>${r.source||'manual'}</b></td>
<td style="text-align:right">${r.total}</td>
<td style="text-align:right">${r.sa_izvorom||0}</td>
<td style="text-align:right">${r.sa_godinom||0}</td>
<td style="text-align:right">${r.sa_adresom||0}</td>
<td style="text-align:right">${r.sa_telefonom||0}</td>
<td style="text-align:right">${r.sa_hns||0}</td>
</tr>`;
});
html += '</tbody></table></div>';
// Purge history
// Sumnjivi - manual bez izvora
if (d.sumnjivi_zapisi && d.sumnjivi_zapisi.length) {
html += `<div class="card" style="margin-bottom:14px">
<h3 style="margin-bottom:10px">⚠️ Sumnjivi zapisi (manual, bez izvora) — top ${d.sumnjivi_zapisi.length}</h3>
<div class="muted" style="font-size:11px;margin-bottom:8px">Klikni za otvoriti profil i validirati podatke s pravim izvorom.</div>
<table class="ri-tbl"><thead><tr><th>ID</th><th>Ime i prezime</th><th>Sport</th><th>Uloga</th><th>Klub</th><th>Quality</th></tr></thead><tbody>`;
d.sumnjivi_zapisi.forEach(s => {
const qcolor = s.quality === 0 ? 'var(--red)' : s.quality === 1 ? 'var(--amber)' : 'var(--text2)';
html += `<tr style="cursor:pointer" onclick="navPush();gotoSportas(${s.id})" ondblclick="navPush();gotoSportas(${s.id})">
<td class="mono">${s.id}</td>
<td><b>${s.ime||'?'} ${s.prezime||'?'}</b></td>
<td>${s.sport||'-'}</td>
<td>${ulogaBadge(s.uloga||'igrac')}</td>
<td class="muted">${s.klub_naziv||'-'}</td>
<td style="color:${qcolor};text-align:center"><b>${s.quality}/4</b></td>
</tr>`;
});
html += '</tbody></table></div>';
}
// Trusted - sa pravim izvorom + datum
if (d.trusted_zapisi && d.trusted_zapisi.length) {
html += `<div class="card" style="margin-bottom:14px">
<h3 style="margin-bottom:10px">✅ Trusted zapisi (potvrđeni izvor + datum rođenja) — top ${d.trusted_zapisi.length}</h3>
<table class="ri-tbl"><thead><tr><th>ID</th><th>Ime i prezime</th><th>Sport</th><th>Klub</th><th>Datum rođenja</th><th>Izvor</th></tr></thead><tbody>`;
d.trusted_zapisi.forEach(s => {
const dob = s.datum_rodenja ? new Date(s.datum_rodenja).toLocaleDateString('hr-HR') : (s.godina_rodenja||'-');
html += `<tr style="cursor:pointer" onclick="navPush();gotoSportas(${s.id})" ondblclick="navPush();gotoSportas(${s.id})">
<td class="mono">${s.id}</td>
<td><b>${s.ime||'?'} ${s.prezime||'?'}</b></td>
<td>${s.sport||'-'}</td>
<td class="muted">${s.klub_naziv||'-'}</td>
<td class="mono">${dob}</td>
<td><span class="risk-low">${s.source}</span></td>
</tr>`;
});
html += '</tbody></table></div>';
}
if (d.purge_history && d.purge_history.length) {
html += `<div class="card"><h3 style="margin-bottom:10px">Povijest čišćenja podataka</h3>
<table class="ri-tbl"><thead><tr><th>Datum</th><th>Akcija</th><th>Target</th><th>Detalji</th></tr></thead><tbody>`;
d.purge_history.forEach(h => {
html += `<tr>
<td class="mono">${h.created_at ? new Date(h.created_at).toLocaleString('hr-HR') : '-'}</td>
<td><code>${h.action}</code></td>
<td>${h.target_text||'-'}</td>
<td style="font-size:10px;color:var(--text2)">${JSON.stringify(h.payload||{}).substring(0,200)}</td>
</tr>`;
});
html += '</tbody></table></div>';
}
c.innerHTML = html;
} catch(e) {
c.innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>';
}
}
buildNavs();
goto('dashboard');
checkRole();
</script>
</body>
</html>