Files
pgz-sport/_backups/index.html.preconsolidate.20260429_082347

3216 lines
171 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="hr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
<meta name="theme-color" content="#0A0E1A">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<!-- v=1777269704 -->
<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');
:root {
--bg: #0d0d0d;
--bg-2: #141414;
--bg-3: #1a1a1a;
--panel: #141414;
--panel-2: #1A2236;
--border: #1e293b;
--border-2: rgba(30,41,59,0.6);
--text: #cbd5e1;
--text-2: #94a3b8;
--text-3: #6B7A99;
--accent: #3b82c4;
--accent-2: #2563a0;
--gold: #f59e0b;
--ok: #2DD4BF;
--warn: #F59E0B;
--crit: #EF4444;
--purple: #A78BFA;
--pink: #F472B6;
--cyan: #22D3EE;
--r: 10px;
--r-sm: 7px;
}
* { 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 -->
<div class="modal-bg" id="login-modal">
<div class="modal">
<h3>🔐 Admin pristup</h3>
<p>Za pristup neprikrivenim podacima (OIB, datum rođenja, e-mail, telefon, IBAN, adresa) potreban je admin token. Bez tokena su osjetljiva polja zamagljena. <b>GDPR čl. 5 i 32.</b></p>
<input class="inp mono" id="token-input" placeholder="Admin token..." autocomplete="off">
<div class="ma">
<button class="btn" style="flex:1" onclick="doLogin()">Prijavi se</button>
<button class="btn sec" onclick="closeLogin()">Odustani</button>
</div>
<div class="hint"><b>Demo:</b><br>Admin: <span class="mono">admin-pgz-2026</span><br>Bez tokena = viewer (privatni podaci zamagljeni)</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:'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:'statistika', label:'Statistika', 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');
}
function showLogin() {
document.getElementById('login-modal').classList.add('show');
// Re-init Google button on each modal open
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'); }
function doLogin() {
const t = document.getElementById('token-input').value.trim();
state.token = t; localStorage.setItem('pgz_token', t);
closeLogin(); checkRole().then(()=>render());
}
async function checkRole() {
try {
const w = await api('/api/whoami');
state.isAdmin = w.role==='admin';
['role-pill','role-pill-mob'].forEach(id => {
const p = document.getElementById(id);
if (!p) return;
if (state.isAdmin) { p.className='role-pill admin'; p.textContent='ADMIN'; p.onclick = () => { state.token=''; localStorage.removeItem('pgz_token'); checkRole(); render(); }; }
else { p.className='role-pill viewer'; p.textContent='VIEWER'; p.onclick = showLogin; }
});
} catch(e) {}
}
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>`; }
}
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 };
// ===== 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() {
document.getElementById('content').innerHTML = `
<div class="page-h"><h2>Korisnici i prava</h2><p class="muted">Multi-tenant · 7 uloga (super_admin → viewer)</p></div>
<div class="card" style="margin-bottom:12px">
<h3>Novi korisnik</h3>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px">
<input id="nuEmail" class="inp" placeholder="email" />
<input id="nuName" class="inp" placeholder="Ime Prezime" />
<input id="nuPwd" class="inp" type="password" placeholder="lozinka" />
<select id="nuRole" class="inp"><option value="viewer">Viewer</option><option value="clan">Član</option><option value="klub_user">Klub User</option><option value="klub_admin">Klub Admin</option><option value="savez_admin">Savez Admin</option><option value="pgz_admin">PGŽ Admin</option></select>
<input id="nuKlub" class="inp" placeholder="klub_id (opcionalno)" />
</div>
<button class="btn primary" style="margin-top:8px" onclick="createUser()"> Kreiraj</button>
<div id="usrStatus" style="margin-top:8px"></div>
</div>
<div class="card"><h3>Postojeći</h3><div id="usrList" class="loader">…</div></div>`;
try {
const us = await v2Fetch('/users?limit=50');
let html = '<table class="t"><tr><th>Email</th><th>Ime</th><th>Uloge</th><th>Klubovi</th><th>Status</th></tr>';
us.forEach(u => html += `<tr><td>${u.email}</td><td>${u.full_name||''}</td><td>${(u.roles||[]).map(r=>`<span class="pill">${r}</span>`).join(' ')}</td><td>${(u.klubovi||[]).length||0}</td><td><span class="pill ${u.status==='active'?'ok':'warn'}">${u.status}</span></td></tr>`);
html += '</table>';
document.getElementById('usrList').innerHTML = html;
} catch(e) { document.getElementById('usrList').innerHTML = '<div class="ban crit">'+e.message+'</div>'; }
}
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;
};
buildNavs();
goto('dashboard');
checkRole();
</script>
</body>
</html>