8116 lines
457 KiB
Plaintext
8116 lines
457 KiB
Plaintext
<!DOCTYPE html>
|
||
<html lang="hr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
|
||
<meta name="theme-color" content="#0A0E1A">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="mobile-web-app-capable" content="yes">
|
||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='12' fill='%230d0d0d'/%3E%3Ctext x='32' y='42' font-family='IBM Plex Sans,sans-serif' font-size='28' font-weight='700' fill='%233b82c4' text-anchor='middle'%3EPG%3C/text%3E%3C/svg%3E">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||
<!-- v=1777465915 -->
|
||
<title>PGŽ Sport · Ri.NET</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;600&display=swap" rel="stylesheet">
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&family=IBM+Plex+Sans:wght@300;400;500;600;700&display=swap');
|
||
/* ═══════════════════════════════════════════
|
||
PGŽ SPORT — RI.NET UNIFIED TOKENS
|
||
Synced with /opt/rinet-v4/app/src/index.css
|
||
v2.0.0 | 29.04.2026
|
||
═══════════════════════════════════════════ */
|
||
:root {
|
||
/* === MASTER TOKENS (klasik-aligned) === */
|
||
--bg: #0d0d0d;
|
||
--bg2: #141414;
|
||
--bg3: #1a1a1a;
|
||
--bg4: #1a2332;
|
||
--bg5: #243044;
|
||
|
||
--border: #1e293b;
|
||
--border2: rgba(30,41,59,0.6);
|
||
--border3: rgba(56, 97, 150, 0.4);
|
||
|
||
--text: #94a3b8;
|
||
--text2: #cbd5e1;
|
||
--text3: #c0c0d0;
|
||
--text-bright: #f1f5f9;
|
||
--text-dim: #6B7A99;
|
||
|
||
--accent: #3b82c4;
|
||
--accent2: #2563a0;
|
||
--accent-glow: rgba(59, 130, 196, 0.12);
|
||
|
||
--green: #22c55e;
|
||
--red: #ef4444;
|
||
--amber: #f59e0b;
|
||
--cyan: #06b6d4;
|
||
|
||
--sans: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
||
--mono: 'JetBrains Mono', 'Fira Code', SF Mono, Consolas, monospace;
|
||
|
||
--radius: 4px;
|
||
--radius-lg: 6px;
|
||
|
||
--chart1: #3b82c4;
|
||
--chart2: #1d6fa8;
|
||
--chart3: #0f5a8e;
|
||
--chart4: #064572;
|
||
|
||
--gradient-main: linear-gradient(135deg, #1a3a5c, #0f2440);
|
||
--gradient-accent: linear-gradient(90deg, #2563a0, #1d6fa8);
|
||
|
||
/* === LEGACY ALIASES (sport-specific, dash variants) === */
|
||
--bg-2: var(--bg2);
|
||
--bg-3: var(--bg3);
|
||
--border-2: var(--border2);
|
||
--accent-2: var(--accent2);
|
||
--text-2: var(--text);
|
||
--text-3: var(--text-dim);
|
||
|
||
/* === SEMANTIC ALIASES === */
|
||
--ok: var(--green);
|
||
--warn: var(--amber);
|
||
--crit: var(--red);
|
||
|
||
/* === DOMAIN COLORS (kept for sport visual variety) === */
|
||
--gold: var(--amber);
|
||
--purple: #A78BFA;
|
||
--pink: #F472B6;
|
||
|
||
/* === LEGACY RADIUS === */
|
||
--r: var(--radius-lg);
|
||
--r-sm: var(--radius);
|
||
|
||
/* === PANEL ALIASES (sport had panel/panel-2) === */
|
||
--panel: var(--bg2);
|
||
--panel-2: var(--bg4);
|
||
}
|
||
/* ═══ MASTER UTILITY CLASSES — append, sport overrides may follow ═══ */
|
||
.ri-card {
|
||
background: var(--bg2);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-lg);
|
||
padding: 12px;
|
||
transition: border-color 0.15s;
|
||
}
|
||
.ri-card:hover { border-color: var(--border2); }
|
||
|
||
.ri-glass {
|
||
background: rgba(8, 12, 20, 0.85);
|
||
backdrop-filter: blur(12px);
|
||
-webkit-backdrop-filter: blur(12px);
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.ri-tbl { width:100%; border-collapse:collapse; font-family:var(--mono); font-size:11px; }
|
||
.ri-tbl th { text-align:left; padding:6px 8px; font-weight:500; font-size:10px; text-transform:uppercase; letter-spacing:0.5px; color:var(--text3); border-bottom:1px solid var(--border2); background:var(--bg3); }
|
||
.ri-tbl td { padding:4px 8px; border-bottom:1px solid var(--border); white-space:nowrap; color:var(--text); }
|
||
.ri-tbl tr:hover td { background: var(--accent-glow); }
|
||
|
||
.ri-btn { font-family:var(--sans); font-size:11px; font-weight:500; padding:6px 12px; border-radius:var(--radius); border:1px solid var(--border2); background:var(--bg3); color:var(--text); cursor:pointer; transition:all 0.15s; letter-spacing:0.02em; }
|
||
.ri-btn:hover { background:var(--bg4); border-color:var(--border3); }
|
||
.ri-btn-primary { background:var(--accent2); border-color:var(--accent); color:white; }
|
||
.ri-btn-primary:hover { background:var(--accent); }
|
||
.ri-btn-ghost { background:transparent; border-color:var(--border); color:var(--text2); }
|
||
|
||
.ri-kpi-value { font-family:var(--mono); font-weight:700; font-size:20px; color:var(--text-bright); letter-spacing:-0.02em; }
|
||
.ri-kpi-label { font-size:9px; text-transform:uppercase; letter-spacing:0.8px; color:var(--text3); margin-top:2px; }
|
||
|
||
.risk { font-family:var(--mono); font-size:9px; font-weight:700; padding:2px 6px; border-radius:2px; }
|
||
.risk-critical { background:rgba(239,68,68,0.12); color:var(--red); }
|
||
.risk-high { background:rgba(212,160,23,0.12); color:var(--amber); }
|
||
.risk-medium { background:rgba(6,182,212,0.12); color:var(--cyan); }
|
||
.risk-low { background:rgba(34,197,94,0.12); color:var(--green); }
|
||
|
||
/* ═══ ICON — Lucide stroke style ═══ */
|
||
.ico-svg, .ri-ico {
|
||
stroke: currentColor;
|
||
stroke-width: 1.5;
|
||
stroke-linecap: round;
|
||
stroke-linejoin: round;
|
||
fill: none;
|
||
flex-shrink: 0;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
|
||
html, body {
|
||
height: 100%;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
font-family: 'IBM Plex Sans', 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
-webkit-font-smoothing: antialiased;
|
||
overflow: hidden;
|
||
}
|
||
.mono { font-family: 'JetBrains Mono', SF Mono, Consolas, monospace; }
|
||
|
||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||
::-webkit-scrollbar-track { background: transparent; }
|
||
::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 4px; }
|
||
|
||
/* === LAYOUT === */
|
||
.app { display: flex; height: 100vh; height: 100dvh; }
|
||
|
||
/* SIDEBAR DESKTOP */
|
||
.sidebar {
|
||
width: 260px;
|
||
background: var(--bg-2);
|
||
border-right: 1px solid var(--border);
|
||
display: flex; flex-direction: column;
|
||
flex-shrink: 0;
|
||
}
|
||
.sb-head { padding: 16px 18px; border-bottom: 1px solid var(--border); }
|
||
.brand { display: flex; align-items: center; gap: 11px; }
|
||
.brand-mark {
|
||
width: 34px; height: 34px;
|
||
background: linear-gradient(135deg, var(--accent), var(--gold));
|
||
border-radius: 8px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
color: white; font-weight: 700; font-size: 13px; letter-spacing: -0.5px;
|
||
box-shadow: 0 2px 12px rgba(59,130,196,0.4);
|
||
}
|
||
.brand-text h1 { font-size: 15px; font-weight: 700; letter-spacing: -0.3px; line-height: 1.2; }
|
||
.brand-text p { font-size: 10px; color: var(--text-3); text-transform: uppercase; letter-spacing: 1.2px; margin-top: 2px; }
|
||
|
||
.role-pill {
|
||
margin-top: 12px; display: inline-flex; align-items: center; gap: 6px;
|
||
padding: 4px 10px; border-radius: 100px;
|
||
font-size: 10px; font-weight: 700; letter-spacing: 0.7px;
|
||
cursor: pointer; transition: 150ms;
|
||
}
|
||
.role-pill::before { content: ''; width: 5px; height: 5px; border-radius: 50%; }
|
||
.role-pill.viewer { background: rgba(59,130,196,0.12); color: var(--accent); border: 1px solid rgba(59,130,196,0.3); }
|
||
.role-pill.viewer::before { background: var(--accent); }
|
||
.role-pill.admin { background: rgba(239,68,68,0.12); color: var(--crit); border: 1px solid rgba(239,68,68,0.3); }
|
||
.role-pill.admin::before { background: var(--crit); }
|
||
|
||
.nav { flex: 1; overflow-y: auto; padding: 8px 0 16px; }
|
||
.nav-sec {
|
||
padding: 12px 18px 4px;
|
||
color: var(--text-3); text-transform: uppercase;
|
||
font-size: 10px; font-weight: 700; letter-spacing: 1.2px;
|
||
}
|
||
.nav-i {
|
||
display: flex; align-items: center; gap: 11px;
|
||
padding: 9px 18px;
|
||
color: var(--text-2); cursor: pointer;
|
||
transition: 150ms;
|
||
border-left: 2px solid transparent;
|
||
font-size: 13px; font-weight: 500;
|
||
}
|
||
.nav-i:hover { background: var(--panel); color: var(--text); }
|
||
.nav-i.active {
|
||
background: linear-gradient(90deg, rgba(59,130,196,0.14) 0%, transparent 100%);
|
||
color: var(--accent);
|
||
border-left-color: var(--accent);
|
||
}
|
||
.nav-i .ico { width: 16px; height: 16px; flex-shrink: 0; }
|
||
.nav-i .b {
|
||
margin-left: auto; background: var(--crit); color: white;
|
||
font-size: 9px; font-weight: 700; padding: 2px 6px;
|
||
border-radius: 100px; min-width: 18px; text-align: center;
|
||
}
|
||
.nav-i .b.warn { background: var(--warn); color: var(--bg); }
|
||
|
||
.sb-foot {
|
||
padding: 11px 18px; border-top: 1px solid var(--border);
|
||
color: var(--text-3); font-size: 10px; line-height: 1.5;
|
||
}
|
||
|
||
/* MAIN */
|
||
.main { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
|
||
|
||
.topbar {
|
||
background: var(--bg-2);
|
||
border-bottom: 1px solid var(--border);
|
||
padding: 12px 20px;
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
gap: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
.tb-title { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
|
||
.tb-bc { font-size: 10px; color: var(--text-3); text-transform: uppercase; letter-spacing: 1.2px; font-weight: 700; }
|
||
.tb-title h2 { font-size: 18px; font-weight: 700; letter-spacing: -0.3px; line-height: 1.3; }
|
||
.tb-meta { color: var(--text-3); font-size: 11px; display: flex; gap: 6px; align-items: center; flex-shrink: 0; }
|
||
.live-dot {
|
||
display: inline-block; width: 6px; height: 6px;
|
||
background: var(--ok); border-radius: 50%;
|
||
animation: pulse 2s infinite;
|
||
}
|
||
@keyframes pulse {
|
||
0%, 100% { box-shadow: 0 0 0 0 rgba(45,212,191,0.55); }
|
||
50% { box-shadow: 0 0 0 5px rgba(45,212,191,0); }
|
||
}
|
||
.menu-btn {
|
||
display: none;
|
||
width: 36px; height: 36px;
|
||
border: 1px solid var(--border); background: var(--panel);
|
||
color: var(--text); border-radius: 8px;
|
||
align-items: center; justify-content: center;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.content { flex: 1; overflow-y: auto; padding: 18px 20px 80px; }
|
||
.content > .inner { max-width: 1500px; margin: 0 auto; }
|
||
|
||
/* GRIDS */
|
||
.grid { display: grid; gap: 12px; }
|
||
.g2 { grid-template-columns: repeat(2, 1fr); }
|
||
.g3 { grid-template-columns: repeat(3, 1fr); }
|
||
.g4 { grid-template-columns: repeat(4, 1fr); }
|
||
|
||
/* CARDS */
|
||
.card {
|
||
background: var(--panel); border: 1px solid var(--border);
|
||
border-radius: var(--r); padding: 14px 16px;
|
||
transition: border-color 150ms;
|
||
}
|
||
.card.acc { border-left: 3px solid var(--accent); }
|
||
.card.gold { border-left: 3px solid var(--gold); }
|
||
.card.ok { border-left: 3px solid var(--ok); }
|
||
.card.warn { border-left: 3px solid var(--warn); }
|
||
.card.crit { border-left: 3px solid var(--crit); }
|
||
|
||
.stat-l {
|
||
color: var(--text-3); font-size: 10px;
|
||
text-transform: uppercase; letter-spacing: 1px; font-weight: 700;
|
||
margin-bottom: 6px;
|
||
display: flex; align-items: center; gap: 5px;
|
||
}
|
||
.stat-v {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 26px; font-weight: 600; letter-spacing: -0.5px; line-height: 1.05;
|
||
}
|
||
.stat-v.sm { font-size: 18px; }
|
||
.stat-v.lg { font-size: 30px; }
|
||
.stat-d { font-size: 10.5px; margin-top: 5px; color: var(--text-3); }
|
||
.stat-d.up { color: var(--ok); }
|
||
.stat-d.down { color: var(--crit); }
|
||
.card.crit .stat-v { color: var(--crit); }
|
||
.card.warn .stat-v { color: var(--warn); }
|
||
.card.ok .stat-v { color: var(--ok); }
|
||
.card.acc .stat-v { color: var(--accent); }
|
||
.card.gold .stat-v { color: var(--gold); }
|
||
|
||
.ct {
|
||
font-size: 11px; font-weight: 700; color: var(--text);
|
||
text-transform: uppercase; letter-spacing: 1px;
|
||
margin-bottom: 12px; padding-bottom: 10px;
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex; justify-content: space-between; align-items: center; gap: 8px;
|
||
}
|
||
.ct .meta { font-size: 10px; color: var(--text-3); font-weight: 500; text-transform: none; letter-spacing: 0; }
|
||
|
||
/* SECTION */
|
||
.sect {
|
||
font-size: 10px; color: var(--text-3);
|
||
text-transform: uppercase; letter-spacing: 1.4px; font-weight: 700;
|
||
margin: 22px 0 10px;
|
||
display: flex; align-items: center; gap: 10px;
|
||
}
|
||
.sect::after { content: ''; flex: 1; height: 1px; background: var(--border); }
|
||
.sect:first-child { margin-top: 0; }
|
||
|
||
/* FILTER BAR */
|
||
.fbar {
|
||
background: var(--panel); border: 1px solid var(--border);
|
||
border-radius: var(--r); padding: 12px 14px;
|
||
margin-bottom: 14px;
|
||
}
|
||
.fbar-t {
|
||
font-size: 10px; color: var(--text-3);
|
||
text-transform: uppercase; letter-spacing: 1.2px; font-weight: 700;
|
||
margin-bottom: 8px;
|
||
}
|
||
.fgrid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||
gap: 8px;
|
||
}
|
||
.fitem label {
|
||
display: block; font-size: 9px; color: var(--text-3);
|
||
text-transform: uppercase; letter-spacing: 0.6px; margin-bottom: 3px; font-weight: 700;
|
||
}
|
||
|
||
/* INPUTS */
|
||
.inp, select {
|
||
background: var(--bg-2); color: var(--text);
|
||
border: 1px solid var(--border);
|
||
padding: 8px 11px; border-radius: var(--r-sm);
|
||
font-size: 13px; outline: none; font-family: inherit;
|
||
width: 100%;
|
||
transition: 150ms;
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
}
|
||
select {
|
||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%236B7A99' d='M6 8L0 0h12z'/%3E%3C/svg%3E");
|
||
background-repeat: no-repeat;
|
||
background-position: right 12px center;
|
||
padding-right: 32px;
|
||
}
|
||
.inp:focus, select:focus { border-color: var(--accent); }
|
||
.inp.flex { flex: 1; min-width: 180px; }
|
||
|
||
.btn {
|
||
background: var(--accent); color: white; border: none;
|
||
padding: 9px 14px; border-radius: var(--r-sm);
|
||
font-size: 12.5px; font-weight: 600; cursor: pointer;
|
||
transition: 150ms; font-family: inherit;
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
}
|
||
.btn:hover { background: var(--accent-2); }
|
||
.btn.sec { background: var(--panel-2); color: var(--text); border: 1px solid var(--border); }
|
||
.btn.sec:hover { border-color: var(--accent); }
|
||
.btn.warn { background: var(--warn); color: var(--bg); }
|
||
.btn.crit { background: var(--crit); }
|
||
.btn.sm { padding: 5px 10px; font-size: 11px; }
|
||
|
||
.toolbar { display: flex; gap: 8px; margin-bottom: 14px; align-items: center; flex-wrap: wrap; }
|
||
|
||
/* TABLES */
|
||
.tbl-wrap {
|
||
background: var(--panel); border: 1px solid var(--border);
|
||
border-radius: var(--r);
|
||
overflow: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
|
||
thead th {
|
||
background: var(--bg-3); color: var(--text-3);
|
||
padding: 10px 12px; text-align: left;
|
||
font-weight: 700; font-size: 9.5px;
|
||
text-transform: uppercase; letter-spacing: 1px;
|
||
cursor: pointer; user-select: none;
|
||
border-bottom: 1px solid var(--border);
|
||
white-space: nowrap;
|
||
position: sticky; top: 0; z-index: 5;
|
||
}
|
||
thead th:hover { color: var(--accent); background: var(--panel-2); }
|
||
thead th.sorted { color: var(--accent); }
|
||
thead th .arr { margin-left: 3px; opacity: 0.5; font-size: 9px; }
|
||
thead th.sorted .arr { opacity: 1; }
|
||
tbody tr {
|
||
border-bottom: 1px solid var(--border);
|
||
cursor: pointer; transition: 100ms;
|
||
}
|
||
tbody tr:hover { background: var(--panel-2); }
|
||
tbody tr:last-child { border-bottom: none; }
|
||
tbody td { padding: 10px 12px; }
|
||
tbody td.num { text-align: right; font-family: 'JetBrains Mono', monospace; font-size: 12px; }
|
||
tbody td.dim { color: var(--text-3); }
|
||
|
||
/* BADGES */
|
||
.bdg {
|
||
display: inline-flex; align-items: center; gap: 4px;
|
||
padding: 2px 7px; border-radius: 4px;
|
||
font-size: 9.5px; font-weight: 700;
|
||
text-transform: uppercase; letter-spacing: 0.5px;
|
||
white-space: nowrap;
|
||
}
|
||
.bdg::before { content: ''; width: 4px; height: 4px; border-radius: 50%; }
|
||
.bdg.ok { background: rgba(45,212,191,0.13); color: var(--ok); }
|
||
.bdg.ok::before { background: var(--ok); }
|
||
.bdg.warn { background: rgba(245,158,11,0.13); color: var(--warn); }
|
||
.bdg.warn::before { background: var(--warn); }
|
||
.bdg.crit { background: rgba(239,68,68,0.13); color: var(--crit); }
|
||
.bdg.crit::before { background: var(--crit); }
|
||
.bdg.info { background: rgba(59,130,196,0.13); color: var(--accent); }
|
||
.bdg.info::before { background: var(--accent); }
|
||
.bdg.gold { background: rgba(245,158,11,0.13); color: var(--gold); }
|
||
.bdg.gold::before { background: var(--gold); }
|
||
.bdg.muted { background: rgba(107,122,153,0.13); color: var(--text-3); }
|
||
.bdg.muted::before { background: var(--text-3); }
|
||
|
||
/* BARS */
|
||
.bar { display: flex; align-items: center; padding: 5px 0; gap: 10px; font-size: 11.5px; }
|
||
.bar .l { width: 36%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 500; }
|
||
.bar .t { flex: 1; height: 6px; background: var(--bg-3); border-radius: 100px; overflow: hidden; }
|
||
.bar .f { height: 100%; background: linear-gradient(90deg, var(--accent), var(--gold)); border-radius: 100px; transition: width 500ms; }
|
||
.bar .f.ok { background: linear-gradient(90deg, var(--ok), var(--cyan)); }
|
||
.bar .f.warn { background: linear-gradient(90deg, var(--warn), #FB923C); }
|
||
.bar .f.crit { background: linear-gradient(90deg, var(--crit), #DC2626); }
|
||
.bar .f.gold { background: linear-gradient(90deg, var(--gold), #E5C064); }
|
||
.bar .v { width: 90px; text-align: right; font-family: 'JetBrains Mono', monospace; font-size: 11px; font-weight: 500; }
|
||
|
||
/* DONUT */
|
||
.donut-w { display: flex; gap: 16px; align-items: center; }
|
||
.donut { width: 110px; height: 110px; position: relative; flex-shrink: 0; }
|
||
.donut svg { transform: rotate(-90deg); }
|
||
.donut-c { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); text-align: center; }
|
||
.donut-c .v { font-size: 20px; font-weight: 700; font-family: 'JetBrains Mono', monospace; line-height: 1; }
|
||
.donut-c .l { font-size: 9px; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.7px; margin-top: 3px; }
|
||
.lg { font-size: 11px; flex: 1; }
|
||
.lg .it { display: flex; align-items: center; gap: 7px; padding: 4px 0; color: var(--text-2); }
|
||
.lg .it .sw { width: 9px; height: 9px; border-radius: 2px; flex-shrink: 0; }
|
||
.lg .it .lname { flex: 1; }
|
||
.lg .it .lval { font-family: 'JetBrains Mono', monospace; color: var(--text); font-weight: 500; }
|
||
|
||
/* BANNER */
|
||
.ban {
|
||
padding: 10px 14px; border-radius: var(--r-sm);
|
||
margin-bottom: 12px; display: flex; gap: 10px;
|
||
align-items: center; font-size: 12.5px;
|
||
border: 1px solid;
|
||
}
|
||
.ban.crit { background: rgba(239,68,68,0.08); border-color: rgba(239,68,68,0.3); color: var(--crit); }
|
||
.ban.warn { background: rgba(245,158,11,0.08); border-color: rgba(245,158,11,0.3); color: var(--warn); }
|
||
.ban.info { background: rgba(59,130,196,0.08); border-color: rgba(59,130,196,0.3); color: var(--accent); }
|
||
.ban.ok { background: rgba(45,212,191,0.08); border-color: rgba(45,212,191,0.3); color: var(--ok); }
|
||
|
||
.empty { text-align: center; color: var(--text-3); padding: 40px 18px; font-size: 13px; }
|
||
.empty-i { font-size: 28px; margin-bottom: 8px; opacity: 0.5; }
|
||
.loader { color: var(--text-3); padding: 50px 20px; text-align: center; font-size: 13px; }
|
||
|
||
/* DRAWER */
|
||
.dr-bg {
|
||
position: fixed; inset: 0;
|
||
background: rgba(10,14,26,0.7);
|
||
backdrop-filter: blur(4px);
|
||
z-index: 90;
|
||
opacity: 0; pointer-events: none;
|
||
transition: 200ms;
|
||
}
|
||
.dr-bg.open { opacity: 1; pointer-events: auto; }
|
||
.drawer {
|
||
position: fixed; top: 0; right: 0;
|
||
width: min(680px, 100%); height: 100vh; height: 100dvh;
|
||
background: var(--bg-2); border-left: 1px solid var(--border);
|
||
overflow-y: auto;
|
||
transform: translateX(100%);
|
||
transition: transform 250ms cubic-bezier(0.4,0,0.2,1);
|
||
z-index: 100;
|
||
box-shadow: -16px 0 48px rgba(0,0,0,0.6);
|
||
}
|
||
.drawer.open { transform: translateX(0); }
|
||
.dr-h {
|
||
padding: 18px 22px; border-bottom: 1px solid var(--border);
|
||
display: flex; justify-content: space-between; align-items: flex-start; gap: 10px;
|
||
position: sticky; top: 0; background: var(--bg-2); z-index: 5;
|
||
}
|
||
.dr-h h3 { font-size: 17px; font-weight: 700; letter-spacing: -0.2px; }
|
||
.dr-h .bc { font-size: 9px; color: var(--text-3); text-transform: uppercase; letter-spacing: 1.2px; margin-bottom: 3px; font-weight: 700; }
|
||
.dr-x {
|
||
background: var(--panel); border: 1px solid var(--border); color: var(--text-3);
|
||
cursor: pointer; width: 30px; height: 30px; border-radius: 8px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 14px; flex-shrink: 0; transition: 150ms;
|
||
}
|
||
.dr-x:hover { color: var(--text); border-color: var(--border-2); }
|
||
.dr-b { padding: 18px 22px 40px; }
|
||
.dr-b dl { display: grid; grid-template-columns: 130px 1fr; gap: 8px 14px; }
|
||
.dr-b dt { color: var(--text-3); font-size: 10px; text-transform: uppercase; letter-spacing: 0.6px; font-weight: 700; padding-top: 1px; }
|
||
.dr-b dd { color: var(--text); font-size: 13px; word-break: break-word; }
|
||
.dr-b dd a { color: var(--accent); text-decoration: none; }
|
||
.dr-b h4 {
|
||
font-size: 10px; color: var(--text-3);
|
||
text-transform: uppercase; letter-spacing: 1.3px; font-weight: 700;
|
||
margin: 22px 0 10px; padding-bottom: 8px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.sub-tbl { width: 100%; font-size: 12px; }
|
||
.sub-tbl td { padding: 7px 10px; border-bottom: 1px solid var(--border); }
|
||
.sub-tbl thead th { padding: 8px 10px; }
|
||
|
||
.blur-tag {
|
||
display: inline-block; margin-left: 5px;
|
||
font-size: 9px; padding: 1px 5px; border-radius: 3px;
|
||
background: rgba(245,158,11,0.15); color: var(--warn);
|
||
text-transform: uppercase; letter-spacing: 0.4px; font-weight: 700;
|
||
}
|
||
|
||
/* MODAL */
|
||
.modal-bg {
|
||
position: fixed; inset: 0;
|
||
background: rgba(10,14,26,0.85);
|
||
backdrop-filter: blur(8px);
|
||
display: none;
|
||
align-items: center; justify-content: center;
|
||
z-index: 1000;
|
||
padding: 20px;
|
||
}
|
||
.modal-bg.show { display: flex; }
|
||
.modal {
|
||
background: var(--panel); border: 1px solid var(--border);
|
||
border-radius: var(--r);
|
||
padding: 22px;
|
||
width: 100%; max-width: 400px;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.6);
|
||
}
|
||
.modal h3 { margin-bottom: 6px; font-size: 17px; }
|
||
.modal p { color: var(--text-3); font-size: 12px; margin-bottom: 14px; line-height: 1.55; }
|
||
.modal .inp { margin-bottom: 10px; }
|
||
.ma { display: flex; gap: 8px; }
|
||
.hint {
|
||
font-size: 11px; color: var(--text-3);
|
||
padding: 9px 11px; background: var(--bg-2); border-radius: 6px;
|
||
margin-top: 10px; line-height: 1.55; border: 1px solid var(--border);
|
||
}
|
||
.hint b { color: var(--gold); }
|
||
|
||
/* MOBILE NAV */
|
||
.mob-nav {
|
||
display: none;
|
||
position: fixed; bottom: 0; left: 0; right: 0;
|
||
background: var(--bg-2);
|
||
border-top: 1px solid var(--border);
|
||
padding: 6px 0 calc(6px + env(safe-area-inset-bottom));
|
||
z-index: 50;
|
||
backdrop-filter: blur(12px);
|
||
}
|
||
.mob-nav-grid { display: grid; grid-template-columns: repeat(5, 1fr); }
|
||
.mob-nav-i {
|
||
display: flex; flex-direction: column; align-items: center; gap: 3px;
|
||
padding: 6px 4px;
|
||
color: var(--text-3);
|
||
cursor: pointer;
|
||
transition: 150ms;
|
||
}
|
||
.mob-nav-i.active { color: var(--accent); }
|
||
.mob-nav-i .ico { width: 20px; height: 20px; }
|
||
.mob-nav-i span { font-size: 9.5px; font-weight: 600; letter-spacing: 0.2px; }
|
||
|
||
/* MOBILE DRAWER NAV */
|
||
.mob-drawer {
|
||
position: fixed; top: 0; left: -300px; width: 280px; height: 100vh; height: 100dvh;
|
||
background: var(--bg-2); border-right: 1px solid var(--border);
|
||
transition: left 250ms; z-index: 200;
|
||
overflow-y: auto;
|
||
display: flex; flex-direction: column;
|
||
}
|
||
.mob-drawer.open { left: 0; }
|
||
|
||
.ico-svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
||
|
||
.footer { padding: 18px 20px; color: var(--text-3); font-size: 10px; text-align: center; border-top: 1px solid var(--border); margin-top: 24px; }
|
||
|
||
/* ===== RESPONSIVE ===== */
|
||
@media (max-width: 880px) {
|
||
.sidebar { display: none; }
|
||
.menu-btn { display: inline-flex; }
|
||
.g4 { grid-template-columns: repeat(2, 1fr); }
|
||
.g3 { grid-template-columns: repeat(2, 1fr); }
|
||
.g2 { grid-template-columns: 1fr; }
|
||
.topbar { padding: 11px 14px; }
|
||
.content { padding: 14px 14px 90px; }
|
||
.mob-nav { display: block; }
|
||
.stat-v { font-size: 22px; }
|
||
.stat-v.lg { font-size: 24px; }
|
||
.stat-v.sm { font-size: 16px; }
|
||
.card { padding: 12px 14px; }
|
||
.ct { font-size: 10.5px; }
|
||
.tb-title h2 { font-size: 16px; }
|
||
.tb-bc { font-size: 9px; }
|
||
.donut-w { flex-direction: column; align-items: stretch; }
|
||
.donut { margin: 0 auto; }
|
||
.dr-b dl { grid-template-columns: 110px 1fr; }
|
||
.modal-bg { align-items: flex-end; padding: 0; }
|
||
.modal { max-width: 100%; border-radius: var(--r) var(--r) 0 0; padding: 22px; padding-bottom: calc(22px + env(safe-area-inset-bottom)); }
|
||
}
|
||
@media (max-width: 460px) {
|
||
.g4 { grid-template-columns: 1fr 1fr; }
|
||
.g3 { grid-template-columns: 1fr; }
|
||
.stat-v { font-size: 20px; }
|
||
.topbar { padding: 10px 12px; }
|
||
.content { padding: 12px 12px 90px; }
|
||
.fgrid { grid-template-columns: 1fr; }
|
||
table { font-size: 11.5px; }
|
||
tbody td, thead th { padding: 8px 10px; }
|
||
}
|
||
|
||
.klub-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 14px; }
|
||
.klub-card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 16px; cursor: pointer; transition: 200ms; }
|
||
.klub-card:hover { transform: translateY(-2px); border-color: var(--accent); box-shadow: 0 4px 16px rgba(0,0,0,0.3); }
|
||
.klub-card.gold-border { border-color: var(--gold); }
|
||
.klub-card-head { display: flex; gap: 12px; margin-bottom: 12px; }
|
||
.klub-logo { width: 44px; height: 44px; border-radius: 9px; background: linear-gradient(135deg, var(--accent), var(--gold)); display: flex; align-items: center; justify-content: center; font-weight: 700; color: var(--bg); font-size: 14px; flex-shrink: 0; }
|
||
.klub-info { flex: 1; min-width: 0; }
|
||
.klub-name { font-size: 14px; font-weight: 600; line-height: 1.3; }
|
||
.klub-savez { font-size: 11px; color: var(--text-dim); }
|
||
.klub-badges { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 12px; }
|
||
.klub-stats-mini { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; padding-top: 10px; border-top: 1px solid var(--border); }
|
||
.klub-stat-mini { text-align: center; }
|
||
.klub-stat-mini .v { font-family: 'JetBrains Mono', monospace; font-size: 16px; font-weight: 600; }
|
||
.klub-stat-mini .l { font-size: 9px; color: var(--text-dim); text-transform: uppercase; margin-top: 3px; }
|
||
.clan-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; }
|
||
.clan-card { background: var(--bg-3); border: 1px solid var(--border); border-radius: 8px; padding: 11px; }
|
||
.clan-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
||
.clan-avatar { width: 32px; height: 32px; border-radius: 50%; background: var(--panel-2); display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 12px; color: var(--accent); }
|
||
.clan-name-x { flex: 1; min-width: 0; }
|
||
.clan-name-x .nm { font-size: 12.5px; font-weight: 600; }
|
||
.clan-name-x .pos { font-size: 10px; color: var(--text-dim); }
|
||
.clan-flags { display: flex; gap: 4px; flex-wrap: wrap; }
|
||
.clan-flag { padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 700; }
|
||
.drawer-stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 16px; }
|
||
.drawer-stat { background: var(--bg-3); border: 1px solid var(--border); border-radius: 7px; padding: 10px; text-align: center; }
|
||
.drawer-stat .v { font-family: 'JetBrains Mono', monospace; font-size: 18px; font-weight: 600; }
|
||
.drawer-stat .l { font-size: 9px; color: var(--text-dim); text-transform: uppercase; margin-top: 4px; }
|
||
.drawer-stat.ok .v { color: var(--ok); }
|
||
.drawer-stat.warn .v { color: var(--warn); }
|
||
.drawer-stat.crit .v { color: var(--crit); }
|
||
.drawer-stat.accent .v { color: var(--accent); }
|
||
.view-toggle { display: inline-flex; gap: 3px; background: var(--bg-2); padding: 3px; border-radius: 7px; border: 1px solid var(--border); }
|
||
.view-toggle button { background: none; border: none; color: var(--text-dim); padding: 5px 10px; font-size: 11px; font-weight: 600; cursor: pointer; border-radius: 5px; font-family: inherit; }
|
||
.view-toggle button.active { background: var(--accent); color: white; }
|
||
@media (max-width: 700px) {
|
||
.klub-grid { grid-template-columns: 1fr; }
|
||
.clan-list { grid-template-columns: 1fr; }
|
||
.drawer-stats-grid { grid-template-columns: repeat(2, 1fr); }
|
||
}
|
||
|
||
|
||
.ekosustav-grid { grid-template-columns: 1fr !important; gap: 12px !important; }
|
||
.ekosustav-coverage-row { font-size: 11px !important; }
|
||
.ekosustav-coverage-row > div:first-child { width: 100px !important; }
|
||
}
|
||
|
||
|
||
/* ===== TOPBAR — 2-row grid layout, mobile-first ===== */
|
||
.topbar { display: flex !important; flex-direction: column; gap: 8px; padding: 10px 14px; min-width: 0; }
|
||
.tb-row { display: flex; align-items: center; gap: 10px; min-width: 0; width: 100%; }
|
||
.tb-row-1 { gap: 12px; }
|
||
.tb-row-2 { padding: 0; }
|
||
.tb-title { flex: 1; min-width: 0; overflow: hidden; }
|
||
.tb-title h2 { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.tb-meta { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--text-dim); white-space: nowrap; flex-shrink: 0; }
|
||
|
||
.tb-search {
|
||
display: flex; align-items: center; gap: 6px; flex: 1; width: 100%;
|
||
background: rgba(255,255,255,0.05); border: 1px solid var(--border);
|
||
border-radius: 8px; padding: 0 10px; height: 36px; position: relative;
|
||
transition: border-color 150ms;
|
||
}
|
||
.tb-search:focus-within { border-color: var(--accent); background: rgba(59,130,196,0.08); }
|
||
.tb-search input {
|
||
flex: 1; min-width: 0; background: transparent; border: 0;
|
||
color: var(--text); font-size: 13px; outline: none; padding: 0 4px; height: 100%;
|
||
}
|
||
.tb-search input::placeholder { color: var(--text-dim); }
|
||
.topbar-go {
|
||
background: var(--accent); color: white; border: 0; border-radius: 5px;
|
||
width: 28px; height: 26px; font-size: 14px; cursor: pointer; line-height: 1;
|
||
flex-shrink: 0;
|
||
}
|
||
.topbar-go:hover { opacity: 0.85; }
|
||
|
||
.top-search-suggest {
|
||
position: absolute; top: calc(100% + 4px); left: 0; right: 0;
|
||
background: var(--panel); border: 1px solid var(--border);
|
||
border-radius: 8px; max-height: 360px; overflow-y: auto;
|
||
z-index: 200; display: none; box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
||
}
|
||
.top-search-suggest.show { display: block; }
|
||
.tss-item { padding: 10px 12px; cursor: pointer; border-bottom: 1px solid var(--border); font-size: 13px; line-height: 1.4; }
|
||
.tss-item:last-child { border-bottom: 0; }
|
||
.tss-item:hover { background: rgba(59,130,196,0.08); }
|
||
.tss-tip {
|
||
display: inline-block; font-size: 9px; color: var(--accent);
|
||
text-transform: uppercase; letter-spacing: 0.5px; margin-right: 8px;
|
||
padding: 2px 5px; background: rgba(59,130,196,0.1); border-radius: 3px;
|
||
}
|
||
|
||
/* Desktop: single row layout */
|
||
@media (min-width: 881px) {
|
||
.topbar { flex-direction: row; align-items: center; gap: 14px; padding: 11px 18px; }
|
||
.tb-row-1 { flex: 0 0 auto; gap: 12px; flex: 1; max-width: 50%; }
|
||
.tb-row-2 { flex: 1; max-width: 480px; padding: 0; }
|
||
.tb-search { max-width: 480px; }
|
||
}
|
||
|
||
/* Mobile: row-2 search full width below row-1 */
|
||
@media (max-width: 880px) {
|
||
.tb-meta { font-size: 10px; }
|
||
.tb-time { display: none; }
|
||
.tb-search { height: 34px; }
|
||
.tb-search input { font-size: 13px; }
|
||
.ekosustav-grid { grid-template-columns: 1fr !important; gap: 14px !important; }
|
||
.ekosustav-coverage-row { font-size: 11px !important; }
|
||
.ekosustav-coverage-row > div:first-child { width: 110px !important; flex-shrink: 0; }
|
||
}
|
||
|
||
|
||
/* ===== V6 PRO FORM (Navision/SAP-style) ===== */
|
||
.v6-form { background:#1a1a1a; border:1px solid #2a2a2a; border-radius:6px; overflow:hidden; }
|
||
.v6-fh { background:linear-gradient(180deg,#2a3a52 0%,#1e2a3e 100%); border-bottom:1px solid #3a4a6a; padding:10px 16px; display:flex; justify-content:space-between; align-items:center; }
|
||
.v6-fh h3 { margin:0; font-size:14px; color:#fff; font-weight:600; }
|
||
.v6-fs { padding:12px 16px; border-bottom:1px solid #2a2a2a; }
|
||
.v6-fs-t { font-size:11px; color:#5e72e4; text-transform:uppercase; letter-spacing:0.5px; margin-bottom:8px; font-weight:600; }
|
||
.v6-g2 { display:grid; grid-template-columns:repeat(2,1fr); gap:8px 16px; }
|
||
.v6-g3 { display:grid; grid-template-columns:repeat(3,1fr); gap:8px 14px; }
|
||
.v6-g4 { display:grid; grid-template-columns:repeat(4,1fr); gap:8px 12px; }
|
||
@media (max-width:700px) { .v6-g2,.v6-g3,.v6-g4 { grid-template-columns:1fr; } }
|
||
.v6-fld { display:flex; flex-direction:column; }
|
||
.v6-w2 { grid-column:span 2; }
|
||
.v6-fld-w { grid-column:1/-1; }
|
||
.v6-lbl { font-size:11px; color:#98a8b8; margin-bottom:3px; font-weight:500; }
|
||
.v6-lbl.req::after { content:' *'; color:#e74c3c; }
|
||
.v6-inp { background:#0f1620; border:1px solid #2a3a4a; color:#e6e8ec; padding:6px 8px; font-size:13px; border-radius:3px; outline:none; font-family:inherit; }
|
||
.v6-inp:focus { border-color:#5e72e4; }
|
||
.v6-inp[readonly] { background:#1a242e; color:#788798; }
|
||
.v6-num { text-align:right; font-family:Consolas,monospace; }
|
||
.v6-calc { background:#1a2a1f !important; color:#4caf50 !important; font-weight:600; }
|
||
.v6-tot { background:linear-gradient(180deg,#1a2a1f 0%,#1e2a24 100%); padding:10px 16px; border-top:2px solid #4caf50; display:flex; justify-content:flex-end; gap:24px; }
|
||
.v6-tot-i { text-align:right; }
|
||
.v6-tot-i .v6-lbl { font-size:10px; }
|
||
.v6-tot-i .v6-val { font-size:18px; font-weight:700; color:#4caf50; font-family:Consolas,monospace; }
|
||
.v6-ac { position:relative; }
|
||
.v6-ac-s { position:absolute; top:100%; left:0; right:0; background:#0f1620; border:1px solid #5e72e4; border-top:none; max-height:200px; overflow-y:auto; z-index:100; display:none; }
|
||
.v6-ac-s.show { display:block; }
|
||
.v6-ac-s div { padding:6px 10px; cursor:pointer; font-size:13px; border-bottom:1px solid #2a2a2a; }
|
||
.v6-ac-s div:hover { background:#2a3a52; color:#fff; }
|
||
.v6-pill { display:inline-block; padding:2px 6px; background:#1a3a52; color:#5e72e4; font-size:10px; border-radius:3px; margin-left:6px; }
|
||
.v6-att-z { border:2px dashed #3a4a6a; border-radius:4px; padding:14px; text-align:center; cursor:pointer; background:#0f1620; }
|
||
.v6-att-z:hover { border-color:#5e72e4; }
|
||
.v6-att-l { margin-top:8px; display:flex; flex-direction:column; gap:4px; }
|
||
.v6-att-i { display:flex; align-items:center; gap:8px; padding:6px 10px; background:#1a2a3a; border-radius:3px; font-size:12px; }
|
||
.v6-att-i .v6-tag { background:#2a5e3a; color:#fff; padding:1px 6px; border-radius:2px; font-size:10px; }
|
||
.v6-att-i .v6-amt { margin-left:auto; font-family:Consolas,monospace; color:#4caf50; }
|
||
|
||
|
||
/* ===== V6.2 VOICE INPUT + CHATBOT ===== */
|
||
.v6-mic-btn {
|
||
background: #2a3a52;
|
||
border: 1px solid #3a4a6a;
|
||
color: #e6e8ec;
|
||
padding: 0 12px;
|
||
border-radius: 3px;
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
height: 100%;
|
||
transition: all 0.2s;
|
||
}
|
||
.v6-mic-btn:hover { background: #5e72e4; }
|
||
.v6-mic-btn.recording {
|
||
background: #c0392b;
|
||
border-color: #e74c3c;
|
||
animation: v6pulse 1.2s infinite;
|
||
}
|
||
@keyframes v6pulse {
|
||
0%, 100% { box-shadow: 0 0 0 0 rgba(231,76,60,0.7); }
|
||
50% { box-shadow: 0 0 0 8px rgba(231,76,60,0); }
|
||
}
|
||
.v6-chat-thread {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
padding: 12px;
|
||
max-height: 60vh;
|
||
overflow-y: auto;
|
||
background: #0f1620;
|
||
border-radius: 6px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.v6-chat-msg {
|
||
padding: 10px 14px;
|
||
border-radius: 12px;
|
||
max-width: 85%;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
word-wrap: break-word;
|
||
}
|
||
.v6-chat-msg.user {
|
||
background: #5e72e4;
|
||
color: #fff;
|
||
align-self: flex-end;
|
||
border-bottom-right-radius: 3px;
|
||
}
|
||
.v6-chat-msg.bot {
|
||
background: #1a2a3a;
|
||
color: #e6e8ec;
|
||
align-self: flex-start;
|
||
border-bottom-left-radius: 3px;
|
||
white-space: pre-wrap;
|
||
}
|
||
.v6-chat-msg.bot .v6-msg-meta {
|
||
font-size: 10px;
|
||
color: #788798;
|
||
margin-bottom: 4px;
|
||
}
|
||
.v6-chat-msg .v6-src-link {
|
||
display: inline-block;
|
||
background: #2a5e3a;
|
||
color: #fff;
|
||
padding: 2px 6px;
|
||
border-radius: 3px;
|
||
font-size: 10px;
|
||
margin: 2px 4px 2px 0;
|
||
cursor: pointer;
|
||
text-decoration: none;
|
||
}
|
||
.v6-chat-msg .v6-src-link:hover { background: #3a7e4a; }
|
||
.v6-chat-typing {
|
||
display: inline-block;
|
||
padding: 8px 14px;
|
||
background: #1a2a3a;
|
||
border-radius: 12px;
|
||
border-bottom-left-radius: 3px;
|
||
}
|
||
.v6-chat-typing span {
|
||
display: inline-block;
|
||
width: 8px;
|
||
height: 8px;
|
||
margin: 0 2px;
|
||
background: #5e72e4;
|
||
border-radius: 50%;
|
||
animation: v6typing 1.4s infinite;
|
||
}
|
||
.v6-chat-typing span:nth-child(2) { animation-delay: 0.2s; }
|
||
.v6-chat-typing span:nth-child(3) { animation-delay: 0.4s; }
|
||
@keyframes v6typing {
|
||
0%, 60%, 100% { opacity: 0.3; transform: translateY(0); }
|
||
30% { opacity: 1; transform: translateY(-4px); }
|
||
}
|
||
.v6-input-row {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: stretch;
|
||
}
|
||
.v6-input-row .inp { flex: 1; }
|
||
|
||
</style>
|
||
|
||
<style id="ri-upgrade-css">
|
||
.ri-modal { position: fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.7); z-index:9999; display:flex; align-items:flex-start; justify-content:center; padding:30px 20px; overflow-y:auto; }
|
||
.ri-modal-box { background:var(--bg); border:1px solid var(--border); border-radius:8px; max-width:1000px; width:100%; box-shadow:0 8px 40px rgba(0,0,0,0.5);
|
||
max-height: 90vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.ri-modal-h { display:flex; justify-content:space-between; align-items:center; padding:14px 18px; border-bottom:1px solid var(--border); background:var(--bg2); border-radius:8px 8px 0 0; }
|
||
.ri-modal-body { padding:14px; max-height:75vh; overflow-y:auto; }
|
||
.ri-icon-btn { background:none; border:none; color:var(--text2); cursor:pointer; padding:6px; border-radius:4px; display:inline-flex; align-items:center; justify-content:center; }
|
||
.ri-icon-btn:hover { background:var(--bg3); color:var(--text); }
|
||
.ri-icon-btn-sm { background:none; border:none; color:var(--text3); cursor:pointer; padding:4px; border-radius:3px; display:inline-flex; }
|
||
.ri-icon-btn-sm:hover { background:var(--bg3); color:var(--accent); }
|
||
.ri-badge { display:inline-block; padding:2px 8px; border-radius:3px; font-size:10px; font-weight:600; }
|
||
.ri-badge-gold { background:rgba(245,158,11,0.15); color:#f59e0b; border:1px solid rgba(245,158,11,0.3); }
|
||
.ri-badge-blue { background:rgba(59,130,246,0.15); color:#3b82f6; border:1px solid rgba(59,130,246,0.3); }
|
||
table.ri-sortable th.ri-sort { user-select:none; cursor:pointer; }
|
||
table.ri-sortable th.ri-sort:hover { color:var(--accent); }
|
||
.ri-sort.num { text-align:right; }
|
||
table.ri-sortable td.num, table.ri-sortable th.num { text-align:right; }
|
||
</style>
|
||
|
||
<style id="ri-actions-css">
|
||
.ri-actions { display:flex; gap:4px; align-items:center; }
|
||
.ri-actions .ri-icon-btn-sm { padding:5px; border:1px solid var(--border); border-radius:4px; background:var(--bg2); color:var(--text2); transition:all 0.15s; }
|
||
.ri-actions .ri-icon-btn-sm:hover { color:var(--accent); border-color:var(--accent); background:var(--bg3); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="app">
|
||
<!-- DESKTOP SIDEBAR -->
|
||
<aside class="sidebar">
|
||
<div class="sb-head">
|
||
<div class="brand">
|
||
<div class="brand-mark">PG</div>
|
||
<div class="brand-text">
|
||
<h1>PGŽ Sport</h1>
|
||
<p>Civic Intelligence OS</p>
|
||
</div>
|
||
</div>
|
||
<div id="role-pill" class="role-pill viewer" onclick="showLogin()">VIEWER</div>
|
||
</div>
|
||
<nav class="nav" id="nav-desktop"></nav>
|
||
<div class="sb-foot">
|
||
<div><b style="color:var(--gold)">Ri.NET</b> · DABI Digital B.V.</div>
|
||
<div>Damir Radulić · 2026</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- MAIN -->
|
||
<main class="main">
|
||
<div id="topbar"></div>
|
||
<div class="content"><div class="inner" id="content"><div class="loader">Učitavanje…</div></div></div>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- MOBILE BOTTOM NAV -->
|
||
<nav class="mob-nav" id="mob-nav"></nav>
|
||
|
||
<!-- MOBILE SIDE DRAWER -->
|
||
<div class="dr-bg" id="mob-drawer-bg" onclick="toggleMobDrawer(false)"></div>
|
||
<aside class="mob-drawer" id="mob-drawer">
|
||
<div class="sb-head">
|
||
<div class="brand">
|
||
<div class="brand-mark">PG</div>
|
||
<div class="brand-text"><h1>PGŽ Sport</h1><p>Civic Intelligence OS</p></div>
|
||
</div>
|
||
<div id="role-pill-mob" class="role-pill viewer" onclick="showLogin()">VIEWER</div>
|
||
</div>
|
||
<nav class="nav" id="nav-mob"></nav>
|
||
<div class="sb-foot">
|
||
<div><b style="color:var(--gold)">Ri.NET</b> · DABI Digital</div>
|
||
<div>Damir Radulić</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- DETAIL DRAWER -->
|
||
<div class="dr-bg" id="drawer-bg" onclick="closeDrawer()"></div>
|
||
<div class="drawer" id="drawer"><div id="drawer-content"></div></div>
|
||
|
||
<!-- LOGIN MODAL — v2 (email+password + admin tab) -->
|
||
<div class="modal-bg" id="login-modal">
|
||
<div class="modal" style="min-width:340px;max-width:420px">
|
||
<div style="display:flex;gap:6px;margin-bottom:14px;border-bottom:1px solid var(--border);padding-bottom:10px">
|
||
<button id="loginTabUser" class="btn" style="flex:1;font-size:12px" onclick="loginSwitchTab('user')">👤 Korisnik</button>
|
||
<button id="loginTabAdmin" class="btn sec" style="flex:1;font-size:12px" onclick="loginSwitchTab('admin')">🔓 Admin token</button>
|
||
</div>
|
||
<div id="loginPanelUser">
|
||
<h3 style="margin-bottom:6px">Prijava korisnika</h3>
|
||
<p class="muted" style="margin-bottom:12px">Email i lozinka. Default lozinka novim korisnicima: <span class="mono">PgzSport2026!</span> (mora se promijeniti)</p>
|
||
<input class="inp" id="loginEmail" type="email" placeholder="email@pgz.hr" autocomplete="username" style="margin-bottom:8px;width:100%">
|
||
<input class="inp" id="loginPwd" type="password" placeholder="Lozinka" autocomplete="current-password" style="margin-bottom:12px;width:100%" onkeydown="if(event.key==='Enter')doUserLogin()">
|
||
<div id="loginError" style="display:none;color:var(--red);font-size:11px;margin-bottom:8px;padding:6px 10px;background:rgba(239,68,68,0.08);border:1px solid rgba(239,68,68,0.3);border-radius:4px"></div>
|
||
<div class="ma">
|
||
<button class="btn" style="flex:1" onclick="doUserLogin()">Prijavi se</button>
|
||
<button class="btn sec" onclick="closeLogin()">Odustani</button>
|
||
</div>
|
||
</div>
|
||
<div id="loginPanelAdmin" style="display:none">
|
||
<h3 style="margin-bottom:6px">Admin token</h3>
|
||
<p class="muted" style="margin-bottom:12px">Za PII unmask (OIB, IBAN, telefon). <b>GDPR čl. 5 i 32.</b></p>
|
||
<input class="inp mono" id="token-input" placeholder="Admin token..." autocomplete="off" style="margin-bottom:12px;width:100%">
|
||
<div class="ma">
|
||
<button class="btn" style="flex:1" onclick="doLogin()">Aktiviraj</button>
|
||
<button class="btn sec" onclick="closeLogin()">Odustani</button>
|
||
</div>
|
||
<div class="hint" style="margin-top:10px"><b>Demo:</b> <span class="mono">admin-pgz-2026</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MUST CHANGE PASSWORD MODAL -->
|
||
<div class="modal-bg" id="pwd-change-modal">
|
||
<div class="modal" style="min-width:340px;max-width:420px">
|
||
<h3>🔒 Promjena lozinke</h3>
|
||
<p class="muted" style="margin-bottom:12px">Vaša početna lozinka mora se promijeniti prije pristupa sustavu.</p>
|
||
<input class="inp" id="pwdNew1" type="password" placeholder="Nova lozinka (min 8 znakova)" autocomplete="new-password" style="margin-bottom:8px;width:100%">
|
||
<input class="inp" id="pwdNew2" type="password" placeholder="Potvrdi novu lozinku" autocomplete="new-password" style="margin-bottom:12px;width:100%" onkeydown="if(event.key==='Enter')doPwdChange()">
|
||
<div id="pwdError" style="display:none;color:var(--red);font-size:11px;margin-bottom:8px"></div>
|
||
<div class="ma">
|
||
<button class="btn" style="flex:1" onclick="doPwdChange()">Promijeni i nastavi</button>
|
||
<button class="btn sec" onclick="doLogout()">Odjavi se</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
|
||
// Sprint3: image proxy for CORS-blocked external slike
|
||
function imgProxy(url) {
|
||
if (!url) return '';
|
||
if (url.startsWith('/') || url.startsWith('data:')) return url;
|
||
if (url.includes('/api/v2/img-proxy')) return url;
|
||
return '/sport/api/v2/img-proxy?u=' + encodeURIComponent(url);
|
||
}
|
||
|
||
const API = '/sport';
|
||
let state = { sort:{}, page:'dashboard', token: localStorage.getItem('pgz_token')||'', filters:{}, isAdmin:false };
|
||
|
||
const fmt = n => n==null||n===''?'–':Number(n).toLocaleString('hr-HR',{maximumFractionDigits:0});
|
||
const fmtEur = n => n==null||n===''?'–':Number(n).toLocaleString('hr-HR',{style:'currency',currency:'EUR',maximumFractionDigits:0});
|
||
const fmtDate = d => d?new Date(d).toLocaleDateString('hr-HR'):'–';
|
||
const debounce = (fn,ms=300) => { let t; return (...a) => { clearTimeout(t); t=setTimeout(()=>fn(...a),ms); }; };
|
||
|
||
async function api(path, opts={}) {
|
||
const headers = { 'Content-Type':'application/json' };
|
||
if (state.token) headers['Authorization'] = 'Bearer '+state.token;
|
||
const res = await fetch(API+path, {...opts, headers:{...headers, ...(opts.headers||{})}});
|
||
if (!res.ok) throw new Error(`API ${res.status}`);
|
||
return res.json();
|
||
}
|
||
|
||
// === NAV CONFIG ===
|
||
const NAV = [
|
||
{ sec:'Pregled', exp:true, items:[
|
||
{ id:'dashboard', label:'Dashboard', mlabel:'Home', badge:false, svg:'<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>' },
|
||
{ id:'alertovi', label:'Alertovi', mlabel:'Alert', badge:true, svg:'<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>' },
|
||
{ id:'statistika', label:'Statistika', mlabel:'Stat', badge:false, svg:'<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>' }
|
||
]},
|
||
{ sec:'Registri', exp:true, items:[
|
||
{ id:'savezi', label:'Savezi', mlabel:'Sav', badge:false, svg:'<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>' },
|
||
{ id:'klubovi', label:'Klubovi', mlabel:'Klu', badge:false, svg:'<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>' },
|
||
{ id:'clanovi', label:'Clanovi', mlabel:'Clan', badge:false, svg:'<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>' },
|
||
{ id:'natjecanja', label:'Natjecanja', mlabel:'Nat', badge:false, svg:'<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>' },
|
||
{ id:'objekti', label:'Sportski objekti', mlabel:'Obj', badge:false, svg:'<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><line x1="3" y1="9" x2="21" y2="9"/>' },
|
||
{ id:'hns', label:'HNS Natjecanja', mlabel:'HNS', badge:false, svg:'<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15 15 0 0 1 4 10 15 15 0 0 1-4 10z"/>' }
|
||
]},
|
||
{ sec:'Financije', exp:false, items:[
|
||
{ id:'proracun', label:'Proracun', mlabel:'Bud', badge:false, svg:'<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>' },
|
||
{ id:'potpore', label:'Potpore', mlabel:'Pot', badge:false, svg:'<polyline points="20 12 20 22 4 22 4 12"/><rect x="2" y="7" width="20" height="5"/><line x1="12" y1="22" x2="12" y2="7"/>' },
|
||
{ id:'rno', label:'Registar NPO', mlabel:'NPO', badge:false, svg:'<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>' },
|
||
{ id:'clanarine', label:'Clanarine', mlabel:'Clar', badge:false, svg:'<rect x="1" y="4" width="22" height="16" rx="2" ry="2"/><line x1="1" y1="10" x2="23" y2="10"/>' },
|
||
{ id:'invoices', label:'Fakture', mlabel:'Fak', badge:false, svg:'<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>' },
|
||
{ id:'expenses', label:'Troskovi', mlabel:'Tro', badge:false, svg:'<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>' }
|
||
]},
|
||
{ sec:'Zdravlje & Pravo', exp:false, items:[
|
||
{ id:'lijecnicki', label:'Lijecnicki', mlabel:'Med', badge:true, svg:'<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>' },
|
||
{ id:'zzjz', label:'ZZJZ', mlabel:'ZZJZ', badge:false, svg:'<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>' },
|
||
{ id:'pravnik', label:'AI Pravnik', mlabel:'Prav', badge:false, svg:'<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>' },
|
||
{ id:'manifestacije',label:'Manifestacije', mlabel:'Man', badge:false, svg:'<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>' }
|
||
]},
|
||
{ sec:'AI & Analitika', exp:false, items:[
|
||
{ id:'ask', label:'AI Chat', mlabel:'AI', badge:false, svg:'<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>' },
|
||
{ id:'godisnjaci', label:'Godisnjaci AI', mlabel:'God', badge:false, svg:'<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>' },
|
||
|
||
{ id:'dms', label:'DMS Dokumenti', mlabel:'DMS', badge:false, svg:'<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>' },
|
||
{ id:'dataQuality', label:'Kvaliteta podataka', mlabel:'KP', badge:false, svg:'<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>' },
|
||
{ id:'graf', label:'Grafovi', mlabel:'Graf', badge:false, svg:'<rect x="2" y="2" width="4" height="20"/><rect x="10" y="8" width="4" height="14"/><rect x="18" y="5" width="4" height="17"/>' },
|
||
{ id:'analytics', label:'Analitika', mlabel:'Ana', badge:false, svg:'<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>' },
|
||
{ id:'search', label:'Napredna pret.', mlabel:'Srch', badge:false, svg:'<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>' }
|
||
]},
|
||
{ sec:'Admin', exp:false, items:[
|
||
{ id:'admin', label:'Administracija', mlabel:'Adm', badge:false, svg:'<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>' }
|
||
]}
|
||
];
|
||
|
||
|
||
function buildNavs() {
|
||
const dEl = document.getElementById('nav-desktop');
|
||
const mEl = document.getElementById('nav-mob');
|
||
if (!dEl) return;
|
||
let dHtml = '';
|
||
const mItems = [];
|
||
|
||
NAV.forEach((s, si) => {
|
||
const isExp = s.exp !== false;
|
||
dHtml += `<div><div class="nav-sec" style="cursor:pointer;display:flex;align-items:center;justify-content:space-between" onclick="toggleNavSec(${si})"><span>${s.sec}</span><span id="nc${si}" style="font-size:9px;opacity:.4;transition:transform .2s;display:inline-block;transform:${isExp?'rotate(90deg)':'rotate(0deg)'}">›</span></div><div id="ns${si}" style="display:${isExp?'block':'none'}">`;
|
||
s.items.forEach(it => {
|
||
const badge = it.badge ? `<span class="b" id="b-${it.id}" style="display:none">0</span>` : '';
|
||
dHtml += `<div class="nav-i" data-page="${it.id}" onclick="goto('${it.id}')"><svg class="ico ico-svg" viewBox="0 0 24 24">${it.svg}</svg><span>${it.label}</span>${badge}</div>`;
|
||
mItems.push(it);
|
||
});
|
||
dHtml += `</div></div>`;
|
||
});
|
||
|
||
dEl.innerHTML = dHtml;
|
||
// Mobile: top 5 most used
|
||
const mob5 = ['dashboard','klubovi','clanovi','lijecnicki','alertovi'];
|
||
if (mEl) mEl.innerHTML = mItems.filter(i => mob5.includes(i.id)).slice(0,5).map(it =>
|
||
`<div class="nav-i" data-page="${it.id}" onclick="goto('${it.id}')"><svg class="ico ico-svg" viewBox="0 0 24 24">${it.svg}</svg><span>${it.mlabel}</span></div>`
|
||
).join('');
|
||
}
|
||
|
||
function toggleNavSec(si) {
|
||
const el = document.getElementById('ns'+si);
|
||
const ch = document.getElementById('nc'+si);
|
||
if (!el) return;
|
||
const vis = el.style.display !== 'none';
|
||
el.style.display = vis ? 'none' : 'block';
|
||
if (ch) ch.style.transform = vis ? 'rotate(0deg)' : 'rotate(90deg)';
|
||
}
|
||
|
||
function goto(page) {
|
||
// Update URL hash for easy sharing/debugging
|
||
if (history.pushState) {
|
||
history.pushState(null, null, '#' + page);
|
||
} else {
|
||
window.location.hash = page;
|
||
}
|
||
state.page = page;
|
||
state.filters = {}; state.sort = {};
|
||
document.querySelectorAll('.nav-i').forEach(e => e.classList.toggle('active', e.dataset.page===page));
|
||
document.querySelectorAll('.mob-nav-i').forEach(e => e.classList.toggle('active', e.dataset.page===page));
|
||
toggleMobDrawer(false);
|
||
render();
|
||
}
|
||
function toggleMobDrawer(open) {
|
||
document.getElementById('mob-drawer').classList.toggle('open', open);
|
||
document.getElementById('mob-drawer-bg').classList.toggle('open', open);
|
||
}
|
||
function openDrawer(html) {
|
||
document.getElementById('drawer-content').innerHTML = html;
|
||
document.getElementById('drawer').classList.add('open');
|
||
document.getElementById('drawer-bg').classList.add('open');
|
||
}
|
||
function closeDrawer() {
|
||
document.getElementById('drawer').classList.remove('open');
|
||
document.getElementById('drawer-bg').classList.remove('open');
|
||
}
|
||
|
||
// ═══ AUTH v3 ═══
|
||
function loginSwitchTab(which) {
|
||
const u = document.getElementById('loginPanelUser'), a = document.getElementById('loginPanelAdmin');
|
||
const tu = document.getElementById('loginTabUser'), ta = document.getElementById('loginTabAdmin');
|
||
if (which === 'user') { u.style.display=''; a.style.display='none'; tu.classList.remove('sec'); ta.classList.add('sec'); }
|
||
else { u.style.display='none'; a.style.display=''; ta.classList.remove('sec'); tu.classList.add('sec'); }
|
||
}
|
||
function showLogin(tab) {
|
||
document.getElementById('login-modal').classList.add('show');
|
||
document.getElementById('loginError').style.display='none';
|
||
loginSwitchTab(tab || 'user');
|
||
setTimeout(()=>{ const el = document.getElementById('loginEmail'); if (el) el.focus(); }, 50);
|
||
if (window.google && window.google.accounts) {
|
||
const btn = document.getElementById('google-signin-btn');
|
||
if (btn && btn.children.length === 0) initGoogleSignIn();
|
||
}
|
||
}
|
||
function closeLogin() { document.getElementById('login-modal').classList.remove('show'); }
|
||
|
||
// Admin token (PII unmask, legacy)
|
||
function doLogin() {
|
||
const t = document.getElementById('token-input').value.trim();
|
||
state.token = t; localStorage.setItem('pgz_token', t);
|
||
closeLogin(); checkRole().then(()=>render());
|
||
}
|
||
|
||
// User login (v2 email+pwd) — primary
|
||
async function doUserLogin() {
|
||
const email = document.getElementById('loginEmail').value.trim();
|
||
const pwd = document.getElementById('loginPwd').value;
|
||
const err = document.getElementById('loginError');
|
||
err.style.display = 'none';
|
||
if (!email || !pwd) { err.textContent='Unesite email i lozinku.'; err.style.display='block'; return; }
|
||
try {
|
||
const r = await fetch('/sport/api/v2/auth/login', {
|
||
method:'POST', headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify({email, password: pwd})
|
||
});
|
||
const d = await r.json();
|
||
if (!r.ok) throw new Error(d.detail || 'Pogrešni podaci');
|
||
localStorage.setItem('rinet_v2_token', d.token);
|
||
localStorage.setItem('rinet_v2_user', JSON.stringify(d.user || {}));
|
||
state.v2Token = d.token; state.v2User = d.user || {};
|
||
closeLogin();
|
||
// must_change_pwd flow
|
||
if (d.user && d.user.must_change_pwd) {
|
||
document.getElementById('pwd-change-modal').classList.add('show');
|
||
return;
|
||
}
|
||
await checkRole();
|
||
render();
|
||
} catch(e) {
|
||
err.textContent = e.message || 'Greška pri prijavi';
|
||
err.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
async function doPwdChange() {
|
||
const p1 = document.getElementById('pwdNew1').value;
|
||
const p2 = document.getElementById('pwdNew2').value;
|
||
const err = document.getElementById('pwdError');
|
||
err.style.display='none';
|
||
if (p1.length < 8) { err.textContent='Lozinka mora imati barem 8 znakova.'; err.style.display='block'; return; }
|
||
if (p1 !== p2) { err.textContent='Lozinke se ne podudaraju.'; err.style.display='block'; return; }
|
||
try {
|
||
const r = await fetch('/sport/api/v2/auth/change-password', {
|
||
method:'POST', headers:{'Content-Type':'application/json','Authorization':'Bearer '+state.v2Token},
|
||
body: JSON.stringify({new_password: p1})
|
||
});
|
||
const d = await r.json();
|
||
if (!r.ok) throw new Error(d.detail || 'Greška');
|
||
document.getElementById('pwd-change-modal').classList.remove('show');
|
||
if (state.v2User) { state.v2User.must_change_pwd = false; localStorage.setItem('rinet_v2_user', JSON.stringify(state.v2User)); }
|
||
await checkRole();
|
||
render();
|
||
} catch(e) { err.textContent=e.message; err.style.display='block'; }
|
||
}
|
||
|
||
async function doLogout() {
|
||
try { await fetch('/sport/api/v2/auth/logout', {method:'POST', headers:{'Authorization':'Bearer '+(state.v2Token||'')}}); } catch(e){}
|
||
localStorage.removeItem('rinet_v2_token');
|
||
localStorage.removeItem('rinet_v2_user');
|
||
state.v2Token = ''; state.v2User = null;
|
||
document.getElementById('pwd-change-modal').classList.remove('show');
|
||
await checkRole();
|
||
render();
|
||
}
|
||
|
||
async function checkRole() {
|
||
// 1) Admin token (legacy PII unmask)
|
||
try {
|
||
const w = await api('/api/whoami');
|
||
state.isAdmin = ['super_admin','pgz_admin','pgz_user'].includes(w.user_type);
|
||
} catch(e) { state.isAdmin = false; }
|
||
|
||
// 2) v2 user session
|
||
state.v2Token = localStorage.getItem('rinet_v2_token') || '';
|
||
try { state.v2User = JSON.parse(localStorage.getItem('rinet_v2_user') || 'null'); } catch(e) { state.v2User = null; }
|
||
|
||
if (state.v2Token) {
|
||
try {
|
||
const r = await fetch('/sport/api/v2/auth/me', { headers: {'Authorization':'Bearer '+state.v2Token} });
|
||
if (r.ok) {
|
||
const me = await r.json();
|
||
state.v2User = me;
|
||
localStorage.setItem('rinet_v2_user', JSON.stringify(me));
|
||
} else if (r.status === 401) {
|
||
localStorage.removeItem('rinet_v2_token');
|
||
localStorage.removeItem('rinet_v2_user');
|
||
state.v2Token = ''; state.v2User = null;
|
||
}
|
||
} catch(e) {}
|
||
}
|
||
|
||
renderRolePill();
|
||
}
|
||
|
||
function renderRolePill() {
|
||
['role-pill', 'role-pill-mob'].forEach(id => {
|
||
const p = document.getElementById(id);
|
||
if (!p) return;
|
||
if (state.v2User && state.v2User.email) {
|
||
const u = state.v2User;
|
||
const ut = u.user_type || (u.roles && u.roles[0]) || 'user';
|
||
const name = u.ime ? (u.ime + (u.prezime ? ' ' + u.prezime[0] + '.' : '')) : (u.full_name || u.email);
|
||
p.className = 'role-pill admin';
|
||
p.innerHTML = '<span style="font-size:9px;opacity:0.7">' + ut.toUpperCase() + '</span><br><span style="font-size:10px">' + name + '</span>';
|
||
p.onclick = () => {
|
||
if (confirm('Odjaviti se?')) doLogout();
|
||
};
|
||
p.title = 'Klik za odjavu (' + u.email + ')';
|
||
} else if (state.isAdmin) {
|
||
p.className = 'role-pill admin';
|
||
p.textContent = 'ADMIN TOKEN';
|
||
p.onclick = () => { state.token = ''; localStorage.removeItem('pgz_token'); checkRole(); render(); };
|
||
} else {
|
||
p.className = 'role-pill viewer';
|
||
p.textContent = '🔐 PRIJAVA';
|
||
p.onclick = () => showLogin('user');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Global 401 handler — redirect to login
|
||
window._origFetch = window.fetch;
|
||
window.fetch = async function(...args) {
|
||
const r = await window._origFetch.apply(this, args);
|
||
if (r.status === 401) {
|
||
const url = (args[0] || '').toString();
|
||
if (url.includes('/api/v2/') && !url.includes('/auth/')) {
|
||
// soft trigger: only show login if we're not already trying to log in
|
||
const m = document.getElementById('login-modal');
|
||
if (m && !m.classList.contains('show')) {
|
||
setTimeout(() => showLogin('user'), 100);
|
||
}
|
||
}
|
||
}
|
||
return r;
|
||
};
|
||
|
||
// State extension
|
||
state.v2Token = localStorage.getItem('rinet_v2_token') || '';
|
||
try { state.v2User = JSON.parse(localStorage.getItem('rinet_v2_user') || 'null'); } catch(e) { state.v2User = null; }
|
||
|
||
// API helper extension — auto-attach v2 bearer for /api/v2/*
|
||
const _origApi = api;
|
||
window.api = async function(path, opts={}) {
|
||
const headers = { 'Content-Type':'application/json' };
|
||
if (path.startsWith('/api/v2/') && state.v2Token) {
|
||
headers['Authorization'] = 'Bearer ' + state.v2Token;
|
||
} else if (state.token) {
|
||
headers['Authorization'] = 'Bearer ' + state.token;
|
||
}
|
||
const res = await fetch(API + path, {...opts, headers:{...headers, ...(opts.headers||{})}});
|
||
if (!res.ok) {
|
||
let detail = '';
|
||
try { detail = (await res.json()).detail || ''; } catch(e) {}
|
||
throw new Error('API ' + res.status + (detail ? ': ' + detail : ''));
|
||
}
|
||
return res.json();
|
||
};
|
||
|
||
|
||
|
||
function globalSearch(q) {
|
||
if (!q || q.length < 2) return;
|
||
state.page = 'search'; state.searchQ = q; render();
|
||
}
|
||
|
||
async function pageSearch() {
|
||
setTopbar('AI Search', 'Rezultati: "' + (state.searchQ || '') + '"');
|
||
// Add enrich button to top
|
||
setTimeout(() => {
|
||
const tb = document.querySelector('.topbar');
|
||
if (tb && !tb.querySelector('.ai-enrich-btn') && state.searchQ) {
|
||
const btn = document.createElement('button');
|
||
btn.className = 'btn primary ai-enrich-btn';
|
||
btn.style.cssText = 'margin-left:14px;display:inline-flex;align-items:center;gap:6px;padding:6px 12px';
|
||
btn.innerHTML = iconExternal() + ' AI obogati pretragu';
|
||
btn.onclick = () => showEnrichModal('search', null, state.searchQ, state.searchQ);
|
||
tb.appendChild(btn);
|
||
}
|
||
}, 100);
|
||
const c = document.getElementById('content');
|
||
if (!state.searchQ) {
|
||
c.innerHTML = '<div style="max-width:680px;margin:30px auto">'
|
||
+ '<div class="card" style="padding:24px">'
|
||
+ '<h3 style="margin:0 0 14px;color:var(--text)">AI Search</h3>'
|
||
+ '<p class="muted" style="margin-bottom:20px">Pretraži klubove, saveze, sportaše, manifestacije, pravilnike i dokumente PGŽ-a.</p>'
|
||
+ '<div style="display:flex;gap:8px;margin-bottom:14px"><input id="aiSearchInline" type="text" placeholder="npr. nogomet Rijeka, kategorizacija, sufinanciranje... ili 🎤 reci" style="flex:1;padding:14px 16px;font-size:15px;background:var(--bg-1);color:var(--text);border:1px solid var(--border-1);border-radius:8px;outline:none" onkeydown="if(event.key===\'Enter\'){state.searchQ=this.value;render()}" autofocus /><button class="v6-mic-btn" style="padding:0 16px;font-size:18px;border-radius:8px" onclick="v6VoiceStart(\'aiSearchInline\', this)" title="Glasovni unos (hr-HR)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></button></div>'
|
||
+ '<div style="display:flex;flex-wrap:wrap;gap:8px">'
|
||
+ ['nogomet','košarka','kategorizacija','pravilnik','financiranje','klub Rijeka','sportaš seniori','manifestacija 2026','medicinski pregled','natjecaji']
|
||
.map(q => '<span class="badge muted" style="cursor:pointer" onclick="state.searchQ=\''+q+'\';render()">' + q + '</span>').join('')
|
||
+ '</div>'
|
||
+ '<div style="margin-top:20px;padding-top:14px;border-top:1px solid var(--border-1);font-size:11px;color:var(--text-dim)">'
|
||
+ 'PGŽ-only po default · 220 saveza · 1622 klubova · 6915 dokumenata · 52k vektora'
|
||
+ '</div></div></div>';
|
||
setTimeout(()=>{const el=document.getElementById('aiSearchInline'); if(el)el.focus()}, 100);
|
||
return;
|
||
}
|
||
c.innerHTML = '<div class="loader">Pretraga BGE-M3...</div>';
|
||
try {
|
||
const tip = state.filters.tip || '';
|
||
const scope = state.filters.searchScope || 'pgz';
|
||
const d = await api('/api/search?q=' + encodeURIComponent(state.searchQ) + '&limit=20&scope=' + scope + (tip ? '&tip=' + tip : ''));
|
||
const tipBadge = { savez:'info', klub:'gold', clan:'muted', manifestacija:'warn', potpora:'ok', proracun:'crit', statistika:'info', dokument:'info', natjecanje:'gold', kategorija:'muted', zakon:'crit' };
|
||
|
||
var hdr = '<div class="filter-bar"><div class="filter-bar-title">Pretraga: <b>'+state.searchQ+'</b></div>'
|
||
+ '<div class="filter-grid" style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">'
|
||
+ '<input id="searchInput" class="inp" value="'+state.searchQ.replace(/"/g,'"')+'" style="flex:1;min-width:200px" onkeydown="if(event.key===\'Enter\'){state.searchQ=this.value;render()}" />'
|
||
+ '<button class="v6-mic-btn" onclick="v6VoiceStart(\'searchInput\', this)" title="Glasovni unos (hr-HR)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></button>'
|
||
+ '<button class="btn primary" onclick="state.searchQ=document.getElementById(\'searchInput\').value;render()">Traži</button>'
|
||
+ '<button class="btn" onclick="state.searchQ=\'\';render()" title="Nova pretraga">Reset</button>'
|
||
+ '<select onchange="state.filters.searchScope=this.value;render()" class="inp" style="min-width:140px">'
|
||
+ '<option value="pgz"'+(scope==='pgz'?' selected':'')+'>Samo PGŽ</option>'
|
||
+ '<option value="all"'+(scope==='all'?' selected':'')+'>Sve (Hrvatska)</option>'
|
||
+ '<option value="national"'+(scope==='national'?' selected':'')+'>Samo nacional</option>'
|
||
+ '</select>'
|
||
+ '<select onchange="state.filters.tip=this.value;render()" class="inp" style="min-width:140px">'
|
||
+ '<option value="">Svi tipovi</option>'
|
||
+ '<option value="klub"'+(tip==='klub'?' selected':'')+'>Klubovi</option>'
|
||
+ '<option value="savez"'+(tip==='savez'?' selected':'')+'>Savezi</option>'
|
||
+ '<option value="dokument"'+(tip==='dokument'?' selected':'')+'>Dokumenti</option>'
|
||
+ '<option value="manifestacija"'+(tip==='manifestacija'?' selected':'')+'>Manifestacije</option>'
|
||
+ '<option value="natjecanje"'+(tip==='natjecanje'?' selected':'')+'>Natjecanja</option>'
|
||
+ '<option value="zakon"'+(tip==='zakon'?' selected':'')+'>Zakoni</option>'
|
||
+ '</select>'
|
||
+ '</div></div>'
|
||
+ '<div style="color:var(--text-dim);font-size:11px;margin-bottom:14px">'+d.count+' rezultata · scope: '+(d.scope||'pgz')+'</div>';
|
||
|
||
c.innerHTML = hdr + '<div class="grid" style="grid-template-columns:1fr;gap:8px">' +
|
||
d.results.map(function(r){
|
||
var url = r.url || (r.payload && (r.payload.source_url || r.payload.url)) || '';
|
||
var title = r.naziv || (r.payload && r.payload.title) || '(bez naslova)';
|
||
var docType = r.payload && r.payload.doc_type;
|
||
var sourceTag = r.payload && r.payload.source;
|
||
var publishDate = r.payload && r.payload.publish_date;
|
||
var relevance = r.relevance || '';
|
||
var click = '';
|
||
var hint = '';
|
||
if (url) { click = 'onclick="window.open(\''+url.replace(/\x27/g,'\\x27')+'\',\'_blank\')" style="cursor:pointer"'; hint = '<span class="pill" style="background:#2a5e3a;color:#fff">otvori dokument</span>'; }
|
||
else if (r.klub_id) { click = 'onclick="showKlub('+r.klub_id+')" style="cursor:pointer"'; hint = '<span class="pill ok">klub →</span>'; }
|
||
else if (r.savez_id) { click = 'onclick="showSavez('+r.savez_id+')" style="cursor:pointer"'; hint = '<span class="pill ok">savez →</span>'; }
|
||
var relB = relevance==='pgz' ? '<span class="pill" style="background:#1a4d3a;color:#27c79b">PGŽ</span>' :
|
||
relevance==='national_doc' ? '<span class="pill" style="background:#3a3a52">nacional</span>' : '';
|
||
return '<div class="card" '+click+'>'
|
||
+ '<div style="display:flex;justify-content:space-between;gap:10px;margin-bottom:6px;flex-wrap:wrap">'
|
||
+ '<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">'
|
||
+ '<span class="badge '+(tipBadge[r.tip]||'muted')+'">'+(r.tip||'?')+'</span>'
|
||
+ relB
|
||
+ (docType ? '<span class="pill muted">'+docType+'</span>' : '')
|
||
+ '<strong>'+title+'</strong>'
|
||
+ hint
|
||
+ '</div>'
|
||
+ '<span class="mono" style="font-size:10px;color:var(--text-dim)">'+(publishDate?publishDate.slice(0,10)+' · ':'')+(sourceTag||'')+' · '+(r.score*100).toFixed(0)+'%</span>'
|
||
+ '</div>'
|
||
+ '<div style="color:var(--text-2);font-size:12px;line-height:1.5">'+((r.tekst||'').slice(0,300))+((r.tekst||'').length>300?'…':'')+'</div>'
|
||
+ (url ? '<div style="margin-top:4px;font-size:11px;color:#5e72e4;word-break:break-all">'+(url.length>120?url.slice(0,120)+'…':url)+'</div>' : '')
|
||
+ '</div>';
|
||
}).join('') +
|
||
(d.count===0 ? '<div class="empty">Nema rezultata. Pokušaj drugi pojam ili promijeni scope.</div>' : '') +
|
||
'</div>';
|
||
} catch (e) { c.innerHTML = '<div class="banner crit">'+e.message+'</div>'; }
|
||
}
|
||
|
||
function setTopbar(bc, title, meta='') {
|
||
document.getElementById('topbar').innerHTML = `
|
||
<div class="topbar">
|
||
<div class="tb-row tb-row-1">
|
||
<button class="menu-btn" onclick="toggleMobDrawer(true)">
|
||
<svg class="ico-svg" viewBox="0 0 24 24" style="width:18px;height:18px"><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
||
</button>
|
||
<div class="tb-title">
|
||
<div class="tb-bc">${bc}</div>
|
||
<h2>${title}</h2>
|
||
</div>
|
||
<div class="tb-meta">${meta} <span class="live-dot"></span><span class="tb-time">${new Date().toLocaleTimeString('hr-HR',{hour:'2-digit',minute:'2-digit'})}</span></div>
|
||
</div>
|
||
<div class="tb-row tb-row-2">
|
||
<div class="tb-search">
|
||
<svg class="ico-svg" viewBox="0 0 24 24" style="width:14px;height:14px;color:var(--text-dim);flex-shrink:0"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.5" y2="16.5"/></svg>
|
||
<input type="search" id="topSearchInput" placeholder="Pretraži klubove, saveze, sportaše..." autocomplete="off"
|
||
oninput="topSearchType(this.value)" onkeydown="if(event.key==='Enter')topSearchGo(this.value)" />
|
||
<button class="v6-mic-btn" style="padding:0 10px;border-radius:0 8px 8px 0;height:auto" onclick="v6VoiceStart('topSearchInput', this)" title="Glasovni unos (hr-HR)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></button>
|
||
<button class="topbar-go" onclick="topSearchGo(document.getElementById('topSearchInput').value)" title="Pretraži">→</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
|
||
let _topSearchTimer = null;
|
||
|
||
function topSearchType(q) {
|
||
clearTimeout(_topSearchTimer);
|
||
if (!q || q.length < 2) {
|
||
const el = document.getElementById('topSearchSuggest');
|
||
if (el) el.classList.remove('show');
|
||
return;
|
||
}
|
||
_topSearchTimer = setTimeout(() => topSearchSuggestFetch(q), 250);
|
||
}
|
||
|
||
async function topSearchSuggestFetch(q) {
|
||
try {
|
||
const r = await fetch('/sport/api/search?q=' + encodeURIComponent(q) + '&limit=8');
|
||
const d = await r.json();
|
||
let el = document.getElementById('topSearchSuggest');
|
||
if (!el) {
|
||
el = document.createElement('div');
|
||
el.id = 'topSearchSuggest';
|
||
el.className = 'top-search-suggest';
|
||
const cont = document.querySelector('.tb-search');
|
||
if (cont) {
|
||
cont.style.position = 'relative';
|
||
cont.appendChild(el);
|
||
}
|
||
}
|
||
if (!d.results || d.results.length === 0) {
|
||
el.innerHTML = '<div class="tss-item dim">Nema rezultata za "' + q + '"</div>';
|
||
} else {
|
||
el.innerHTML = d.results.map(function(r) {
|
||
var p = r.payload || {};
|
||
var tip = r.tip || p.tip || '';
|
||
var naziv = r.naziv || p.naziv || p.title || '?';
|
||
var url = r.url || p.source_url || p.url || '';
|
||
var id = r.klub_id || r.savez_id || (p && (p.klub_id || p.savez_id)) || '';
|
||
var score = r.score ? r.score.toFixed(2) : '';
|
||
var onClick;
|
||
if (url) {
|
||
var safeUrl = url.replace(/\x27/g, '%27').replace(/"/g, '%22');
|
||
onClick = 'window.open(\x27' + safeUrl + '\x27, \x27_blank\x27)';
|
||
} else if (tip === 'klub' && id) onClick = 'showKlub(' + id + ')';
|
||
else if (tip === 'savez' && id) onClick = 'showSavez(' + id + ')';
|
||
else if (tip === 'savez') onClick = 'goto(\x27savezi\x27)';
|
||
else { var sq = q.replace(/\x27/g, ''); onClick = 'state.searchQ=\x27' + sq + '\x27; goto(\x27search\x27)'; }
|
||
var safeNaziv = (naziv || '?').replace(/</g,'<').replace(/>/g,'>');
|
||
if (safeNaziv.length > 60) safeNaziv = safeNaziv.slice(0,60) + '…';
|
||
return '<div class="tss-item" onclick="' + onClick + ';document.getElementById(\x27topSearchSuggest\x27).classList.remove(\x27show\x27)">' +
|
||
'<span class="tss-tip">' + tip + '</span>' +
|
||
'<span>' + safeNaziv + '</span>' +
|
||
(url ? '<span class="dim" style="font-size:10px;margin-left:6px">📄</span>' : '') +
|
||
(score ? '<span class="dim" style="float:right;font-size:10px">' + score + '</span>' : '') +
|
||
'</div>';
|
||
}).join('');
|
||
}
|
||
el.classList.add('show');
|
||
} catch (e) { console.error('topSearch err', e); }
|
||
}
|
||
|
||
function topSearchGo(q) {
|
||
if (!q) return;
|
||
state.searchQ = q;
|
||
const el = document.getElementById('topSearchSuggest');
|
||
if (el) el.classList.remove('show');
|
||
goto('search');
|
||
}
|
||
|
||
document.addEventListener('click', function(e) {
|
||
const search = document.querySelector('.tb-search');
|
||
if (search && !search.contains(e.target)) {
|
||
const sug = document.getElementById('topSearchSuggest');
|
||
if (sug) sug.classList.remove('show');
|
||
}
|
||
});
|
||
|
||
function tableHeader(cols, sortKey) {
|
||
return cols.map(c => {
|
||
const s = c.sort !== false;
|
||
const sorted = state.sort[sortKey] && state.sort[sortKey].col === c.key ? 'sorted' : '';
|
||
const arr = sorted ? (state.sort[sortKey].order==='asc'?'↑':'↓') : '↕';
|
||
return `<th class="${sorted}" ${s?`onclick="sortBy('${sortKey}','${c.key}')"`:''}>${c.label}${s?` <span class="arr">${arr}</span>`:''}</th>`;
|
||
}).join('');
|
||
}
|
||
function sortBy(sk, col) {
|
||
const s = state.sort[sk] || {col:'', order:'asc'};
|
||
if (s.col===col) s.order = s.order==='asc'?'desc':'asc';
|
||
else { s.col=col; s.order='asc'; }
|
||
state.sort[sk] = s; render();
|
||
}
|
||
function getSort(sk) { const s = state.sort[sk]; return s && s.col ? `&sort=${s.col}&order=${s.order}` : ''; }
|
||
|
||
function donut(values, labels, colors, totalDisplay, label) {
|
||
const sum = values.reduce((a,b)=>a+b,0) || 1;
|
||
const r = 46, c = 2*Math.PI*r;
|
||
let off = 0;
|
||
const segs = values.map((v,i) => {
|
||
const len = (v/sum)*c;
|
||
const seg = `<circle r="${r}" cx="55" cy="55" fill="transparent" stroke="${colors[i]}" stroke-width="12" stroke-dasharray="${len} ${c-len}" stroke-dashoffset="${-off}"/>`;
|
||
off += len; return seg;
|
||
}).join('');
|
||
return `<div class="donut"><svg width="110" height="110" viewBox="0 0 110 110">${segs}</svg>
|
||
<div class="donut-c"><div class="v">${totalDisplay}</div><div class="l">${label||''}</div></div></div>`;
|
||
}
|
||
|
||
function lineChart(series, labels, w=600, h=200, colors=['#4A9EFF','#D4A852','#A78BFA','#F472B6','#2DD4BF','#22D3EE','#F59E0B']) {
|
||
const pad = {l:50, r:14, t:12, b:28};
|
||
const iw = w-pad.l-pad.r, ih = h-pad.t-pad.b;
|
||
const all = series.flatMap(s=>s.data);
|
||
const max = Math.max(...all,1)*1.05, min = 0;
|
||
const xs = iw / Math.max(labels.length-1, 1);
|
||
const lines = series.map((s,si) => {
|
||
const pts = s.data.map((v,i) => `${pad.l+i*xs},${pad.t+ih-(v-min)/(max-min)*ih}`).join(' ');
|
||
return `<polyline fill="none" stroke="${colors[si%colors.length]}" stroke-width="2.5" points="${pts}" stroke-linejoin="round"/>` +
|
||
s.data.map((v,i) => `<circle cx="${pad.l+i*xs}" cy="${pad.t+ih-(v-min)/(max-min)*ih}" r="3" fill="${colors[si%colors.length]}" stroke="var(--bg)" stroke-width="1.5"/>`).join('');
|
||
}).join('');
|
||
const xax = labels.map((l,i) => `<text x="${pad.l+i*xs}" y="${h-10}" fill="var(--text-3)" font-size="10" font-family="JetBrains Mono" text-anchor="middle">${l}</text>`).join('');
|
||
const yt = [0,0.25,0.5,0.75,1].map(p => {
|
||
const y = pad.t+ih*(1-p);
|
||
return `<line x1="${pad.l}" y1="${y}" x2="${w-pad.r}" y2="${y}" stroke="var(--border)" stroke-dasharray="3,3" opacity="0.5"/>
|
||
<text x="${pad.l-6}" y="${y+3}" fill="var(--text-3)" font-size="9" font-family="JetBrains Mono" text-anchor="end">${fmt(min+p*(max-min))}</text>`;
|
||
}).join('');
|
||
const lg = series.map((s,i) => `<div class="it"><div class="sw" style="background:${colors[i%colors.length]}"></div><span class="lname">${s.label}</span></div>`).join('');
|
||
return `<svg viewBox="0 0 ${w} ${h}" style="width:100%;height:auto" preserveAspectRatio="xMidYMid meet">${yt}${lines}${xax}</svg>
|
||
<div class="lg" style="display:flex;gap:14px;flex-wrap:wrap;margin-top:8px">${lg}</div>`;
|
||
}
|
||
|
||
function barChart(items, getLbl, getVal, fillClass='', formatter=fmt) {
|
||
const max = Math.max(...items.map(getVal),1);
|
||
return items.map(it => `<div class="bar">
|
||
<div class="l" title="${getLbl(it)}">${getLbl(it)}</div>
|
||
<div class="t"><div class="f ${fillClass}" style="width:${(getVal(it)/max*100).toFixed(1)}%"></div></div>
|
||
<div class="v">${formatter(getVal(it))}</div></div>`).join('');
|
||
}
|
||
|
||
// ========== PAGES ==========
|
||
|
||
async function fetchEkosustav() {
|
||
try {
|
||
const e = await api('/api/dashboard/ekosustav');
|
||
const c = e.coverage || {};
|
||
const rows = [
|
||
['🆔 OIB', c.oib_pct, e.s_oib, e.klubova_total],
|
||
['👤 Predsjednik', c.predsjednik_pct, e.s_predsjednik, e.klubova_total],
|
||
['🎯 Ciljevi', c.ciljevi_pct, e.s_ciljevi, e.klubova_total],
|
||
['📋 Djelatnosti', c.opis_pct, e.s_opis, e.klubova_total],
|
||
['🏛️ Savez', c.savez_pct, e.s_savez, e.klubova_total],
|
||
['📍 Sjedište', c.sjediste_pct, e.s_sjediste, e.klubova_total],
|
||
['👥 Tajnik', c.tajnik_pct, e.s_tajnik, e.klubova_total],
|
||
['📧 Email', c.email_pct, e.s_email, e.klubova_total],
|
||
];
|
||
const barColor = (pct) => pct >= 80 ? 'var(--ok)' : (pct >= 50 ? 'var(--gold)' : 'var(--accent)');
|
||
const coverageHTML = rows.map(([label, pct, count, total]) => `
|
||
<div class="ekosustav-coverage-row" style="display:flex;align-items:center;gap:10px;font-size:12px;padding:6px 0;border-bottom:1px solid var(--border)">
|
||
<div style="width:120px;color:var(--text)">${label}</div>
|
||
<div style="flex:1;background:rgba(255,255,255,0.04);border-radius:3px;height:18px;position:relative;overflow:hidden">
|
||
<div style="position:absolute;top:0;left:0;height:100%;width:${pct}%;background:${barColor(pct)};opacity:0.4"></div>
|
||
<div style="position:absolute;top:0;left:0;height:100%;width:100%;display:flex;align-items:center;justify-content:space-between;padding:0 8px;font-size:11px">
|
||
<span class="mono" style="color:var(--text)">${pct}%</span>
|
||
<span class="dim" style="font-size:10px">${count}/${total}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
const sportTopHTML = (e.by_sport || []).slice(0, 8).map(s =>
|
||
`<div style="display:flex;justify-content:space-between;font-size:12px;padding:3px 0">
|
||
<span class="dim">${s.sport}</span><span class="mono">${s.broj}</span>
|
||
</div>`).join('');
|
||
|
||
const regionHTML = (e.by_region || []).map(r => {
|
||
const pctR = ((r.broj / e.klubova_total) * 100).toFixed(0);
|
||
return `<div style="display:flex;justify-content:space-between;font-size:12px;padding:3px 0">
|
||
<span>${r.region}</span><span class="mono dim">${r.broj} (${pctR}%)</span></div>`;
|
||
}).join('');
|
||
|
||
return `<div style="background:linear-gradient(135deg,rgba(59,130,196,0.08),rgba(245,158,11,0.05));border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:16px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
||
<h3 style="margin:0;font-size:14px;color:var(--text)">🌐 Sport Ekosustav PGŽ — FINA Registar Coverage</h3>
|
||
<span class="bdg gold" style="font-size:11px">${e.klubova_total} sport klubova</span>
|
||
</div>
|
||
<div class="ekosustav-grid" style="display:grid;grid-template-columns:1fr 280px;gap:20px">
|
||
<div>${coverageHTML}</div>
|
||
<div>
|
||
<div style="margin-bottom:10px">
|
||
<div style="font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px">Po regiji</div>
|
||
${regionHTML}
|
||
</div>
|
||
<div>
|
||
<div style="font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.5px;margin:10px 0 6px">Top sportovi</div>
|
||
${sportTopHTML}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
} catch(e) { return ''; }
|
||
}
|
||
|
||
async function pageDashboard() {
|
||
|
||
// ERP KPI cards above existing content
|
||
const c = document.getElementById('content');
|
||
const erpHeader = document.createElement('div');
|
||
erpHeader.id = 'erp-kpi-grid';
|
||
erpHeader.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px;margin-bottom:16px';
|
||
erpHeader.innerHTML = '<div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px;text-align:center"><div style="font-size:11px;color:#475569;text-transform:uppercase;margin-bottom:4px">Klubovi</div><div id="kpi-klubovi" style="font-size:24px;font-weight:700;color:#3b82f6">—</div></div><div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px;text-align:center"><div style="font-size:11px;color:#475569;text-transform:uppercase;margin-bottom:4px">Savezi</div><div id="kpi-savezi" style="font-size:24px;font-weight:700;color:#06b6d4">—</div></div><div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px;text-align:center"><div style="font-size:11px;color:#475569;text-transform:uppercase;margin-bottom:4px">Sportaši</div><div id="kpi-sportasi" style="font-size:24px;font-weight:700;color:#22c55e">—</div></div><div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px;text-align:center"><div style="font-size:11px;color:#475569;text-transform:uppercase;margin-bottom:4px">Nositelji ★</div><div id="kpi-nositelji" style="font-size:24px;font-weight:700;color:#f59e0b">—</div></div><div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px;text-align:center"><div style="font-size:11px;color:#475569;text-transform:uppercase;margin-bottom:4px">Proračun</div><div id="kpi-proracun" style="font-size:24px;font-weight:700;color:#a78bfa">—</div></div><div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px;text-align:center"><div style="font-size:11px;color:#475569;text-transform:uppercase;margin-bottom:4px">Dokumenti</div><div id="kpi-dokumenti" style="font-size:24px;font-weight:700;color:#f97316">—</div></div>';
|
||
if (c && !document.getElementById('erp-kpi-grid')) c.prepend(erpHeader);
|
||
// Load KPIs
|
||
api('/api/dashboard').then(d => {
|
||
if (!d) return;
|
||
document.getElementById('kpi-klubovi') && (document.getElementById('kpi-klubovi').textContent = d.aktivnih_klubova || '—');
|
||
document.getElementById('kpi-savezi') && (document.getElementById('kpi-savezi').textContent = d.aktivnih_saveza || '—');
|
||
document.getElementById('kpi-sportasi') && (document.getElementById('kpi-sportasi').textContent = (d.aktivnih_clanova||0).toLocaleString('hr-HR'));
|
||
document.getElementById('kpi-nositelji') && (document.getElementById('kpi-nositelji').textContent = d.nositelja_kvalitete || '—');
|
||
if (d.proracun_aktualni) document.getElementById('kpi-proracun') && (document.getElementById('kpi-proracun').textContent = '€' + (d.proracun_aktualni/1e6).toFixed(2) + 'M');
|
||
});
|
||
api('/api/v2/dokumenti/list').then(d => {
|
||
const n = (d && (d.rows||d||[]).length) || 0;
|
||
document.getElementById('kpi-dokumenti') && (document.getElementById('kpi-dokumenti').textContent = n);
|
||
}).catch(()=>{});
|
||
|
||
setTopbar('PGŽ Sportski savez', 'Operativni pregled');
|
||
const c = document.getElementById('content');
|
||
c.innerHTML = '<div class="loader">Učitavanje…</div>';
|
||
const ekoHTML = await fetchEkosustav();
|
||
try {
|
||
const godina = state.filters.godina || 2026;
|
||
const savez = state.filters.savez_id || '';
|
||
const region = state.filters.region || '';
|
||
const [d, savezi] = await Promise.all([
|
||
api(`/api/dashboard?godina=${godina}` + (savez?`&savez_id=${savez}`:'') + (region?`®ion=${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> </label>
|
||
<button class="btn sec" onclick="state.filters={};render()">↻ Reset</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
${d.critical_alerts>0?`<div class="ban crit"><b>⚠</b> ${d.critical_alerts} kritičnih alerta</div>`:''}
|
||
${d.warning_alerts>0?`<div class="ban warn"><b>⚠</b> ${d.warning_alerts} upozorenja</div>`:''}
|
||
|
||
<div class="sect">Ključni indikatori</div>
|
||
<div class="grid g4">
|
||
<div class="card acc"><div class="stat-l">Aktivni savezi</div><div class="stat-v">${d.aktivnih_saveza}</div><div class="stat-d">županijska razina</div></div>
|
||
<div class="card acc"><div class="stat-l">Aktivni klubovi</div><div class="stat-v">${d.aktivnih_klubova}</div><div class="stat-d">u 28 saveza</div></div>
|
||
<div class="card gold"><div class="stat-l">Nositelji kvalitete</div><div class="stat-v">${d.nositelja_kvalitete}</div><div class="stat-d">elitni klubovi</div></div>
|
||
<div class="card ok"><div class="stat-l">Proračun ${godina}</div><div class="stat-v sm">${fmtEur(d.proracun_aktualni)}</div><div class="stat-d up">↗ +38% YoY</div></div>
|
||
</div>
|
||
|
||
<div class="grid g4" style="margin-top:12px">
|
||
<div class="card"><div class="stat-l">Registr. sportaši</div><div class="stat-v">${fmt(d.registriranih_sportasa)}</div></div>
|
||
<div class="card"><div class="stat-l">Treneri</div><div class="stat-v">${fmt(d.trenera)}</div></div>
|
||
<div class="card"><div class="stat-l">Reprezentativci</div><div class="stat-v">${fmt(d.reprezentativaca)}</div></div>
|
||
<div class="card"><div class="stat-l">Aktivni članovi</div><div class="stat-v">${fmt(d.aktivnih_clanova)}</div></div>
|
||
</div>
|
||
|
||
<div class="sect">Financije ${godina}.</div>
|
||
<div class="grid g3">
|
||
<div class="card ok"><div class="stat-l">Naplaćeno</div><div class="stat-v sm">${fmtEur(d.naplaceno_clanarine_god)}</div></div>
|
||
<div class="card warn"><div class="stat-l">Dug članarine</div><div class="stat-v sm">${fmtEur(d.dug_clanarine_god)}</div></div>
|
||
<div class="card acc"><div class="stat-l">ZZJZ isplata</div><div class="stat-v sm">${fmtEur(d.zzjz_isplata_god)}</div></div>
|
||
</div>
|
||
|
||
<div class="sect">Liječnički pregledi</div>
|
||
<div class="grid g3">
|
||
<div class="card crit"><div class="stat-l">Istekli</div><div class="stat-v">${fmt(d.isteki_lijecnicki)}</div></div>
|
||
<div class="card warn"><div class="stat-l">Ističu uskoro</div><div class="stat-v">${fmt(d.lijecnicki_uskoro_istek)}</div></div>
|
||
<div class="card crit"><div class="stat-l">Critical alertovi</div><div class="stat-v">${fmt(d.critical_alerts)}</div></div>
|
||
</div>
|
||
|
||
<div class="sect">Vizualne analize</div>
|
||
|
||
<div class="grid g2">
|
||
<div class="card">
|
||
<div class="ct">📈 Proračun PGŽ za sport <span class="meta">2016—2026</span></div>
|
||
${lineChart([{label:'Ukupno', data:procV}], procY, 600, 220)}
|
||
</div>
|
||
<div class="card">
|
||
<div class="ct">📊 Top 5 saveza · trend reg. <span class="meta">${allG[0]||''}—${allG[allG.length-1]||''}</span></div>
|
||
${trSer.length?lineChart(trSer, allG, 600, 220):'<div class="empty">Bez podataka</div>'}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid g2" style="margin-top:12px">
|
||
<div class="card">
|
||
<div class="ct">🏆 Top saveza · reg. ${godina>2024?2024:godina}.</div>
|
||
${barChart(topSv, s=>s.naziv, s=>s.registriranih||0)}
|
||
</div>
|
||
<div class="card">
|
||
<div class="ct">⭐ Nositelji kvalitete ${godina>2025?2025:godina}.</div>
|
||
${noslist.length?barChart(noslist, n=>n.naziv_kluba, n=>parseFloat(n.iznos||0), 'gold', fmtEur):'<div class="empty">Bez podataka</div>'}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid g3" style="margin-top:12px">
|
||
<div class="card">
|
||
<div class="ct">🏥 Status pregleda</div>
|
||
<div class="donut-w">
|
||
${donut(lsV, ['Validni','Uskoro','Istekli','Bez termina'], ['#2DD4BF','#F59E0B','#EF4444','#6B7A99'], lsV.reduce((a,b)=>a+b,0), 'pregleda')}
|
||
<div class="lg">
|
||
<div class="it"><div class="sw" style="background:#2DD4BF"></div><span class="lname">Validni</span><span class="lval">${lsV[0]}</span></div>
|
||
<div class="it"><div class="sw" style="background:#F59E0B"></div><span class="lname">Uskoro</span><span class="lval">${lsV[1]}</span></div>
|
||
<div class="it"><div class="sw" style="background:#EF4444"></div><span class="lname">Istekli</span><span class="lval">${lsV[2]}</span></div>
|
||
<div class="it"><div class="sw" style="background:#6B7A99"></div><span class="lname">Bez termina</span><span class="lval">${lsV[3]}</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="ct">⚕ ZZJZ podjela</div>
|
||
<div class="donut-w">
|
||
${donut([parseFloat(zzjz.zzjz_udio||0), parseFloat(zzjz.klub_udio||0), parseFloat(zzjz.clan_udio||0)],
|
||
['ZZJZ','Klub','Član'], ['#2DD4BF','#4A9EFF','#D4A852'],
|
||
fmt(parseFloat(zzjz.total||0)), 'EUR')}
|
||
<div class="lg">
|
||
<div class="it"><div class="sw" style="background:#2DD4BF"></div><span class="lname">ZZJZ</span><span class="lval">${fmtEur(zzjz.zzjz_udio)}</span></div>
|
||
<div class="it"><div class="sw" style="background:#4A9EFF"></div><span class="lname">Klub</span><span class="lval">${fmtEur(zzjz.klub_udio)}</span></div>
|
||
<div class="it"><div class="sw" style="background:#D4A852"></div><span class="lname">Član</span><span class="lval">${fmtEur(zzjz.clan_udio)}</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="ct">👥 Kategorije</div>
|
||
<div class="donut-w">
|
||
${donut(kat.map(k=>parseInt(k.cnt)), kat.map(k=>k.kategorija), katC, totK, 'članova')}
|
||
<div class="lg">${kat.map((k,i)=>`<div class="it"><div class="sw" style="background:${katC[i%katC.length]}"></div><span class="lname">${k.kategorija||'–'}</span><span class="lval">${k.cnt}</span></div>`).join('')}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
${cG.length?`<div class="card" style="margin-top:12px">
|
||
<div class="ct">💰 Članarine kroz godine · propisano vs naplaćeno vs dug</div>
|
||
${lineChart([
|
||
{label:'Propisano', data:cG.map(c=>parseFloat(c.propisano||0))},
|
||
{label:'Naplaćeno', data:cG.map(c=>parseFloat(c.placeno||0))},
|
||
{label:'Dug', data:cG.map(c=>parseFloat(c.dug||0))}
|
||
], cG.map(c=>c.godina), 1200, 220)}</div>`:''}
|
||
`;
|
||
} catch (e) { c.innerHTML = `<div class="ban crit"><b>Greška:</b> ${e.message}</div>`; }
|
||
}
|
||
|
||
// ========== ANALYTICS FINAL — Modern Dropdown UI ==========
|
||
let _anaOpts = null;
|
||
|
||
async function pageAnalytics() {
|
||
const el = document.getElementById('content');
|
||
el.innerHTML = '<div class="loader">Ucitavanje...</div>';
|
||
|
||
const [opts, trend] = await Promise.all([
|
||
api('/api/v2/analytics/filter-options').catch(()=>{}),
|
||
api('/api/v2/analytics/proracun-trend').catch(()=>[])
|
||
]);
|
||
_anaOpts = opts || {};
|
||
|
||
const last = (trend||[]).slice(-1)[0] || {};
|
||
const rastPct = last.rast_pct || 0;
|
||
|
||
el.innerHTML = `
|
||
<div class="page-h">
|
||
<h2>Bodovanje & Analitika</h2>
|
||
<p class="muted">Sustav bodovanja za dodjelu proracunskih sredstava PGZ · HOO kriteriji</p>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:16px">
|
||
${[
|
||
['Proracun 2026', _eurF(last.ukupno_eur), rastPct > 0 ? '#22c55e' : '#ef4444'],
|
||
['Rast', (rastPct>0?'+':'')+rastPct+'%', rastPct>0?'#22c55e':'#ef4444'],
|
||
['Saveza rangirano', '<span id="kpi-n">—</span>', 'var(--text)'],
|
||
['Avg evid. %', '<span id="kpi-evid">—</span>', 'var(--text)'],
|
||
].map(([label,val,col])=>`
|
||
<div class="card" style="padding:12px 14px;text-align:center;background:var(--bg4)">
|
||
<div style="font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:5px">${label}</div>
|
||
<div style="font-size:18px;font-weight:700;font-family:monospace;color:${col}">${val}</div>
|
||
</div>`).join('')}
|
||
</div>
|
||
|
||
<div class="card" style="padding:10px 12px;margin-bottom:12px;display:flex;flex-wrap:wrap;gap:8px;align-items:center;background:var(--bg3)">
|
||
<div style="display:flex;align-items:center;gap:6px">
|
||
<span style="font-size:11px;color:var(--muted);white-space:nowrap">Prikaz</span>
|
||
<select id="av" class="i" onchange="_anaLoad()" style="min-width:150px">
|
||
<option value="savezi">Bodovanje saveza</option>
|
||
<option value="klubovi">Bodovanje klubova</option>
|
||
<option value="trend">Trend proracuna</option>
|
||
</select>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:6px">
|
||
<span style="font-size:11px;color:var(--muted)">Sport</span>
|
||
<select id="aspt" class="i" onchange="_anaLoad()" style="min-width:130px">
|
||
<option value="">Svi sportovi</option>
|
||
${(_anaOpts.sportovi_savezi||[]).map(s=>`<option>${s}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:6px">
|
||
<span style="font-size:11px;color:var(--muted)">Grad</span>
|
||
<select id="agrd" class="i" onchange="_anaLoad()" style="min-width:110px">
|
||
<option value="">Svi</option>
|
||
${(_anaOpts.gradovi||[]).map(g=>`<option>${g}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:6px">
|
||
<span style="font-size:11px;color:var(--muted)">Min. cl.</span>
|
||
<select id="amc" class="i" onchange="_anaLoad()">
|
||
<option value="0">Svi</option>
|
||
<option value="10">10+</option>
|
||
<option value="50">50+</option>
|
||
<option value="100">100+</option>
|
||
<option value="500">500+</option>
|
||
</select>
|
||
</div>
|
||
<button class="btn" onclick="_anaCsv()" style="margin-left:auto;font-size:11px;padding:5px 12px">↓ CSV</button>
|
||
<button onclick="navigate('dataQuality')" style="background:#0f172a;border:1px solid #f59e0b44;color:#f59e0b;padding:5px 10px;border-radius:6px;cursor:pointer;font-size:11px">📊 Kompletnost</button>
|
||
<button onclick="navigate('dms')" style="background:#0f172a;border:1px solid #3b82f644;color:#3b82f6;padding:5px 10px;border-radius:6px;cursor:pointer;font-size:11px">📁 DMS</button>
|
||
</div>
|
||
|
||
<div id="anaContent" class="loader">Ucitavanje...</div>
|
||
|
||
<div id="anaDrill" style="display:none;margin-top:12px">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
|
||
<button class="btn" onclick="_drillClose()" style="font-size:11px;padding:4px 10px">←</button>
|
||
<span id="drillTitle" style="font-size:14px;font-weight:700;color:var(--text)"></span>
|
||
</div>
|
||
<div id="drillContent" class="loader">Ucitavanje...</div>
|
||
</div>`;
|
||
|
||
_anaLoad();
|
||
}
|
||
|
||
function _eurF(n) {
|
||
if (!n && n!==0) return '—';
|
||
return new Intl.NumberFormat('hr-HR',{style:'currency',currency:'EUR',maximumFractionDigits:0}).format(n);
|
||
}
|
||
|
||
function _scoreBar(s) {
|
||
s = Math.max(0, Math.min(100, s||0));
|
||
const c = s>=60?'#22c55e':s>=30?'#f59e0b':'#ef4444';
|
||
return `<div style="display:flex;align-items:center;gap:5px">
|
||
<div style="width:50px;height:5px;background:rgba(255,255,255,0.08);border-radius:3px;overflow:hidden;flex-shrink:0">
|
||
<div style="width:${s}%;height:100%;background:${c}"></div>
|
||
</div>
|
||
<b style="font-size:12px;font-family:monospace;color:${c}">${s}</b>
|
||
</div>`;
|
||
}
|
||
|
||
async function _anaLoad() {
|
||
const view = document.getElementById('av')?.value||'savezi';
|
||
const sport = document.getElementById('aspt')?.value||'';
|
||
const grad = document.getElementById('agrd')?.value||'';
|
||
const mc = document.getElementById('amc')?.value||'0';
|
||
const el = document.getElementById('anaContent'); if(!el)return;
|
||
el.style.display='block';
|
||
document.getElementById('anaDrill').style.display='none';
|
||
el.innerHTML='<div class="loader">Ucitavanje...</div>';
|
||
|
||
if (view==='trend') {
|
||
const d=await api('/api/v2/analytics/proracun-trend').catch(()=>[]);
|
||
_renderTrend(d,el); return;
|
||
}
|
||
|
||
const ep = view==='savezi'
|
||
? '/api/v2/analytics/budget-score?'+new URLSearchParams({sport,min_clanova:mc})
|
||
: '/api/v2/analytics/klub-score?'+new URLSearchParams({sport,min_clanova:mc});
|
||
|
||
const data = await api(ep).catch(()=>[]);
|
||
const rows = grad ? (data||[]).filter(r=>r.grad===grad) : (data||[]);
|
||
|
||
if(view==='savezi') {
|
||
document.getElementById('kpi-n').textContent=rows.length;
|
||
if(rows.length) {
|
||
const avg=rows.reduce((a,r)=>a+(r.pct_s_dob||0),0)/rows.length;
|
||
const kpi=document.getElementById('kpi-evid');
|
||
kpi.textContent=avg.toFixed(1)+'%';
|
||
kpi.style.color=avg>50?'#22c55e':avg>20?'#f59e0b':'#ef4444';
|
||
}
|
||
_renderSavezi(rows,el);
|
||
} else {
|
||
_renderKlubovi(rows,el);
|
||
}
|
||
}
|
||
|
||
function _renderSavezi(data,el) {
|
||
if(!data?.length){el.innerHTML='<p class="muted" style="padding:20px">Nema saveza za filtere.</p>';return;}
|
||
el.innerHTML=`<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:12px">
|
||
<thead><tr style="background:var(--bg3)">
|
||
${['#','Savez','Reg.','Sustav','Klubovi','Treneri','Repr.','Evid.%','Score'].map(h=>`<th style="padding:7px 10px;color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.5px;text-align:${['Savez'].includes(h)?'left':'right'};${h==='Score'||h==='#'?'text-align:left':''}">${h}</th>`).join('')}
|
||
</tr></thead>
|
||
<tbody>${data.map((r,i)=>`
|
||
<tr style="border-top:1px solid var(--border);cursor:pointer;transition:background .1s" onclick="drillSavez(${r.id},'${(r.naziv||'').replace(/'/g,"\\'")}')" onmouseover="this.style.background='rgba(255,255,255,0.025)'" onmouseout="this.style.background=''">
|
||
<td style="padding:7px 10px;color:var(--muted);font-size:11px;font-family:monospace">${i+1}</td>
|
||
<td style="padding:7px 10px">
|
||
<div style="font-weight:500;color:var(--text)">${r.naziv||'—'}</div>
|
||
${r.sport?`<div style="font-size:10px;color:var(--muted)">${r.sport}${r.grad?' · '+r.grad:''}</div>`:''}
|
||
</td>
|
||
<td style="padding:7px 10px;text-align:right;font-family:monospace">${r.registriranih||'—'}</td>
|
||
<td style="padding:7px 10px;text-align:right;font-family:monospace">${r.clanova_u_sustavu||0}</td>
|
||
<td style="padding:7px 10px;text-align:right;font-family:monospace">${r.klubova||0}</td>
|
||
<td style="padding:7px 10px;text-align:right;font-family:monospace">${r.trenera||0}</td>
|
||
<td style="padding:7px 10px;text-align:right;font-family:monospace">${r.reprezentativaca||0}</td>
|
||
<td style="padding:7px 10px;text-align:right;font-size:11px;color:${(r.pct_s_dob||0)>=50?'#22c55e':(r.pct_s_dob||0)>=20?'#f59e0b':'#ef4444'}">${r.pct_s_dob||0}%</td>
|
||
<td style="padding:7px 10px">${_scoreBar(r.score_ukupno)}</td>
|
||
</tr>`).join('')}
|
||
</tbody></table></div>
|
||
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:10px;padding:8px 10px;background:var(--bg3);border-radius:6px;font-size:11px;color:var(--muted)">
|
||
<span>Clanovi <b>25</b></span><span>Klubovi <b>15</b></span><span>Treneri <b>15</b></span><span>Evidencija <b>20</b></span><span>Repr. <b>15</b></span>
|
||
<span style="margin-left:auto;color:var(--accent)">Klik = detalji</span>
|
||
</div>`;
|
||
}
|
||
|
||
function _renderKlubovi(data,el) {
|
||
if(!data?.length){el.innerHTML='<p class="muted" style="padding:20px">Nema klubova.</p>';return;}
|
||
el.innerHTML=`<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:12px">
|
||
<thead><tr style="background:var(--bg3)">
|
||
${['#','Klub','Savez','Clan.','M','Z','DOB%','Score'].map(h=>`<th style="padding:7px 10px;color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.5px;text-align:${['Klub','Savez'].includes(h)?'left':'right'};${h==='#'||h==='Score'?'text-align:left':''}">${h}</th>`).join('')}
|
||
</tr></thead>
|
||
<tbody>${data.map((r,i)=>`<tr style="border-top:1px solid var(--border)">
|
||
<td style="padding:6px 10px;color:var(--muted);font-size:11px;font-family:monospace">${i+1}</td>
|
||
<td style="padding:6px 10px"><div style="color:var(--text)">${r.naziv||'—'}</div><div style="font-size:10px;color:var(--muted)">${r.grad||''}</div></td>
|
||
<td style="padding:6px 10px;font-size:11px;color:var(--text2)">${r.savez||'—'}</td>
|
||
<td style="padding:6px 10px;text-align:right;font-family:monospace">${r.n_clanova||0}</td>
|
||
<td style="padding:6px 10px;text-align:right;font-family:monospace;color:var(--text2)">${r.muski||0}</td>
|
||
<td style="padding:6px 10px;text-align:right;font-family:monospace;color:var(--text2)">${r.zenski||0}</td>
|
||
<td style="padding:6px 10px;text-align:right;font-size:11px;color:${(r.pct_dob||0)>=50?'#22c55e':(r.pct_dob||0)>=20?'#f59e0b':'#ef4444'}">${r.pct_dob||0}%</td>
|
||
<td style="padding:6px 10px">${_scoreBar(r.score)}</td>
|
||
</tr>`).join('')}</tbody>
|
||
</table></div>`;
|
||
}
|
||
|
||
function _renderTrend(data,el) {
|
||
if(!data?.length){el.innerHTML='<p class="muted">Nema podataka.</p>';return;}
|
||
const maxV=Math.max(...data.map(r=>r.ukupno_eur||0));
|
||
el.innerHTML=`<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:12px">
|
||
<thead><tr style="background:var(--bg3)">
|
||
<th style="text-align:left;padding:7px 10px;color:var(--muted);font-size:10px;text-transform:uppercase">Godina</th>
|
||
<th style="text-align:right;padding:7px 10px;color:var(--muted);font-size:10px;text-transform:uppercase">PGZ</th>
|
||
<th style="text-align:right;padding:7px 10px;color:var(--muted);font-size:10px;text-transform:uppercase">Ministarstvo</th>
|
||
<th style="text-align:right;padding:7px 10px;color:var(--muted);font-size:10px;text-transform:uppercase">Ukupno</th>
|
||
<th style="text-align:right;padding:7px 10px;color:var(--muted);font-size:10px;text-transform:uppercase">Rast</th>
|
||
<th style="padding:7px 10px;color:var(--muted);font-size:10px;text-transform:uppercase;min-width:120px">Vizual</th>
|
||
</tr></thead>
|
||
<tbody>${[...data].reverse().map(r=>{
|
||
const pct=maxV>0?(r.ukupno_eur||0)/maxV*100:0;
|
||
const rc=(r.rast_pct||0)>0?'#22c55e':'#ef4444';
|
||
return `<tr style="border-top:1px solid var(--border)">
|
||
<td style="padding:7px 10px;font-weight:700;color:var(--text)">${r.godina}</td>
|
||
<td style="padding:7px 10px;text-align:right;font-family:monospace">${_eurF(r.pgz_ukupno_eur)}</td>
|
||
<td style="padding:7px 10px;text-align:right;font-family:monospace;color:var(--text2)">${_eurF(r.ministarstvo_eur)}</td>
|
||
<td style="padding:7px 10px;text-align:right;font-family:monospace;font-weight:700">${_eurF(r.ukupno_eur)}</td>
|
||
<td style="padding:7px 10px;text-align:right;font-family:monospace;color:${rc}">${r.rast_pct!=null?(r.rast_pct>0?'+':'')+r.rast_pct+'%':'—'}</td>
|
||
<td style="padding:7px 10px"><div style="height:8px;background:rgba(255,255,255,0.06);border-radius:2px;overflow:hidden">
|
||
<div style="width:${pct}%;height:100%;background:var(--accent);opacity:.75"></div>
|
||
</div></td>
|
||
</tr>`;
|
||
}).join('')}</tbody>
|
||
</table></div>`;
|
||
}
|
||
|
||
async function drillSavez(id, naziv) {
|
||
document.getElementById('anaContent').style.display='none';
|
||
const drill=document.getElementById('anaDrill');
|
||
drill.style.display='block';
|
||
document.getElementById('drillTitle').textContent=naziv;
|
||
const el=document.getElementById('drillContent');
|
||
el.innerHTML='<div class="loader">Ucitavanje...</div>';
|
||
|
||
const d=await api('/api/v2/analytics/savez-drill?savez_id='+id).catch(()=>null);
|
||
if(!d||d.error){el.innerHTML='<p class="muted">Nema podataka.</p>';return;}
|
||
|
||
const s=d.savez||{}, cs=d.clanovi_spol||{};
|
||
const pctDob=cs.ukupno>0?((cs.s_dob||0)/cs.ukupno*100).toFixed(1)+'%':'—';
|
||
|
||
el.innerHTML=`
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin-bottom:12px">
|
||
${[['Clanovi',cs.ukupno||0],['Muski',cs.muski||0],['Zenski',cs.zenski||0],['DOB %',pctDob],['Sport',s.sport||'—'],['Sjediste',s.grad||'—']].map(([k,v])=>`
|
||
<div class="card" style="padding:10px 12px;background:var(--bg4);text-align:center">
|
||
<div style="font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:4px">${k}</div>
|
||
<div style="font-size:15px;font-weight:700;color:var(--text)">${v}</div>
|
||
</div>`).join('')}
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px">
|
||
<div>
|
||
<div style="font-size:10px;font-weight:700;color:var(--accent);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px">Statistika po godinama</div>
|
||
<table style="width:100%;border-collapse:collapse;font-size:11px">
|
||
<thead><tr style="background:var(--bg3)"><th style="padding:4px 8px;text-align:left">God.</th><th style="padding:4px 8px;text-align:right">Reg.</th><th style="padding:4px 8px;text-align:right">Tren.</th><th style="padding:4px 8px;text-align:right">Repr.</th></tr></thead>
|
||
<tbody>${(d.statistike||[]).map(r=>`<tr style="border-top:1px solid var(--border)"><td style="padding:4px 8px;font-weight:600">${r.godina}</td><td style="padding:4px 8px;text-align:right;font-family:monospace">${r.registriranih||0}</td><td style="padding:4px 8px;text-align:right;font-family:monospace">${r.trenera||0}</td><td style="padding:4px 8px;text-align:right;font-family:monospace">${r.reprezentativaca||0}</td></tr>`).join('')||'<tr><td colspan="4" style="padding:8px;color:var(--muted)">—</td></tr>'}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div>
|
||
<div style="font-size:10px;font-weight:700;color:var(--accent);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px">Top klubovi</div>
|
||
<table style="width:100%;border-collapse:collapse;font-size:11px">
|
||
<thead><tr style="background:var(--bg3)"><th style="padding:4px 8px;text-align:left">Klub</th><th style="padding:4px 8px;text-align:right">Cl.</th><th style="padding:4px 8px;text-align:right">DOB</th></tr></thead>
|
||
<tbody>${(d.klubovi||[]).slice(0,8).map(k=>{
|
||
const p=k.n_clanova>0?((k.s_dob||0)/k.n_clanova*100).toFixed(0):'?';
|
||
return`<tr style="border-top:1px solid var(--border)"><td style="padding:4px 8px;color:var(--text2)">${k.naziv||'—'}</td><td style="padding:4px 8px;text-align:right;font-family:monospace">${k.n_clanova||0}</td><td style="padding:4px 8px;text-align:right;font-family:monospace;color:${p>=50?'#22c55e':p>=20?'#f59e0b':'#ef4444'}">${p}%</td></tr>`;
|
||
}).join('')||'<tr><td colspan="3" style="padding:8px;color:var(--muted)">—</td></tr>'}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
${d.javne_potrebe?.length?`
|
||
<div style="font-size:10px;font-weight:700;color:var(--accent);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px">Javne dodjele</div>
|
||
<table style="width:100%;border-collapse:collapse;font-size:11px">
|
||
<thead><tr style="background:var(--bg3)"><th style="padding:4px 8px;text-align:left">Godina</th><th style="padding:4px 8px;text-align:left">Namjena</th><th style="padding:4px 8px;text-align:right">EUR</th></tr></thead>
|
||
<tbody>${d.javne_potrebe.map(j=>`<tr style="border-top:1px solid var(--border)"><td style="padding:4px 8px;font-weight:600">${j.godina}</td><td style="padding:4px 8px;color:var(--text2)">${j.naslov||j.vrsta||'—'}</td><td style="padding:4px 8px;text-align:right;font-family:monospace;font-weight:600">${_eurF(j.iznos_eur)}</td></tr>`).join('')}
|
||
</tbody>
|
||
</table>`:
|
||
'<div style="font-size:11px;color:var(--muted)">Nema evidencije javnih dodjela.</div>'}`;
|
||
}
|
||
|
||
function _drillClose() {
|
||
document.getElementById('anaDrill').style.display='none';
|
||
document.getElementById('anaContent').style.display='block';
|
||
}
|
||
|
||
function _anaCsv() {
|
||
const t=document.querySelector('#anaContent table'); if(!t)return;
|
||
const rows=[...t.querySelectorAll('tr')].map(r=>[...r.querySelectorAll('th,td')].map(c=>'"'+c.textContent.trim().replace(/"/g,'""')+'"').join(','));
|
||
const a=document.createElement('a');
|
||
a.href='data:text/csv;charset=utf-8,'+encodeURIComponent('\uFEFF'+rows.join('\n'));
|
||
a.download='pgz_bodovanje_'+new Date().toISOString().slice(0,10)+'.csv';
|
||
a.click();
|
||
}
|
||
|
||
|
||
async function pageGraf() {
|
||
setTopbar && setTopbar('3D Graf', 'PGŽ Sport Intelligence · Savezi · Klubovi · Sportaši');
|
||
const el = document.getElementById('content');
|
||
el.innerHTML = `
|
||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:10px" id="g3d-ctrl">
|
||
<select id="g3d-mode" onchange="graf3dLoad()" style="background:#0d1117;color:#e2e8f0;border:1px solid #1e293b;padding:6px 10px;border-radius:6px;font-size:12px">
|
||
<option value="sport">Savezi → Klubovi (Sport)</option>
|
||
<option value="osobe">Savezi → Osobe</option>
|
||
<option value="geo">Gradovi → Klubovi</option>
|
||
</select>
|
||
<select id="g3d-sport" onchange="graf3dLoad()" style="background:#0d1117;color:#e2e8f0;border:1px solid #1e293b;padding:6px 10px;border-radius:6px;font-size:12px">
|
||
<option value="">Svi sportovi</option>
|
||
${['nogomet','košarka','odbojka','rukomet','bočanje','skijanje','vaterpolo','atletika','tenis','jedrenje','karate','plivanje'].map(s=>`<option value="${s}">${s}</option>`).join('')}
|
||
</select>
|
||
<span id="g3d-info" style="color:#475569;font-size:11px;margin-left:auto"></span>
|
||
</div>
|
||
<div id="g3d-container" style="width:100%;height:calc(100vh-200px);min-height:500px;border-radius:12px;overflow:hidden;border:1px solid #1e293b;background:#0d0d0d;position:relative">
|
||
<div id="g3d-loader" style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#475569;font-size:14px;background:#0d0d0d;z-index:5">
|
||
Učitavam 3D graf...
|
||
</div>
|
||
</div>
|
||
<div id="g3d-legend" style="display:flex;gap:16px;margin-top:8px;font-size:11px;color:#64748b;padding:0 4px">
|
||
<span><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#06b6d4;margin-right:4px"></span>Savez/Grad</span>
|
||
<span><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#22c55e;margin-right:4px"></span>Klub</span>
|
||
<span><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#ec4899;margin-right:4px"></span>Osoba</span>
|
||
<span><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#f59e0b;margin-right:4px"></span>Nositelj ★</span>
|
||
</div>`;
|
||
|
||
await graf3dLoadLib();
|
||
await graf3dLoad();
|
||
}
|
||
|
||
let _g3d = null;
|
||
let _g3dLib = false;
|
||
|
||
async function graf3dLoadLib() {
|
||
if (_g3dLib) return;
|
||
await Promise.all([
|
||
new Promise(r => {
|
||
const s = document.createElement('script');
|
||
s.src = 'https://unpkg.com/3d-force-graph@1.73.2/dist/3d-force-graph.min.js';
|
||
s.onload = r; document.head.appendChild(s);
|
||
}),
|
||
new Promise(r => {
|
||
const s = document.createElement('script');
|
||
s.src = 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js';
|
||
s.onload = r; document.head.appendChild(s);
|
||
})
|
||
]);
|
||
_g3dLib = true;
|
||
}
|
||
|
||
async function graf3dLoad() {
|
||
const mode = document.getElementById('g3d-mode')?.value || 'sport';
|
||
const sport = document.getElementById('g3d-sport')?.value || '';
|
||
const info = document.getElementById('g3d-info');
|
||
const loader = document.getElementById('g3d-loader');
|
||
if (loader) loader.style.display = 'flex';
|
||
|
||
const [savezi, klubovi, clanovi] = await Promise.all([
|
||
api('/api/savezi').catch(()=>({rows:[]})),
|
||
api('/api/klubovi?' + (sport?'sport='+encodeURIComponent(sport):'')).catch(()=>({rows:[]})),
|
||
mode === 'osobe' ? api('/api/clanovi?limit=200').catch(()=>({rows:[]})) : Promise.resolve({rows:[]})
|
||
]);
|
||
|
||
const saveziArr = savezi.rows || savezi || [];
|
||
const klubArr = (klubovi.rows || klubovi || []).filter(k=>k.naziv && k.naziv.length > 2);
|
||
const clanoviArr = clanovi.rows || clanovi || [];
|
||
|
||
const nodes = [], links = [];
|
||
const nodeMap = {};
|
||
|
||
if (mode === 'sport') {
|
||
// Savezi → Klubovi force graph
|
||
saveziArr.slice(0, 40).forEach(s => {
|
||
const id = 'savez_' + s.id;
|
||
nodes.push({ id, name: (s.naziv||'Savez').slice(0,30), group:'savez', val:20, color:'#06b6d4' });
|
||
nodeMap[s.id] = id;
|
||
});
|
||
klubArr.slice(0, 300).forEach(k => {
|
||
const id = 'klub_' + k.id;
|
||
const col = k.nositelj_kvalitete ? '#f59e0b' : '#22c55e';
|
||
nodes.push({ id, name: (k.naziv||'Klub').slice(0,25), group:'klub', val:k.nositelj_kvalitete?10:5, color:col, _data:k });
|
||
const savezId = nodeMap[k.savez_id];
|
||
if (savezId) links.push({ source: savezId, target: id, color: 'rgba(34,197,94,0.15)' });
|
||
});
|
||
|
||
} else if (mode === 'osobe') {
|
||
// Savezi → Osobe network
|
||
saveziArr.slice(0, 20).forEach(s => {
|
||
const id = 'savez_' + s.id;
|
||
nodes.push({ id, name: (s.naziv||'Savez').slice(0,30), group:'savez', val:25, color:'#06b6d4' });
|
||
nodeMap[s.id] = id;
|
||
});
|
||
clanoviArr.forEach(c => {
|
||
if (!c.ime && !c.prezime) return;
|
||
const name = ((c.prezime||'') + ' ' + (c.ime||'')).trim();
|
||
const id = 'clan_' + c.id;
|
||
nodes.push({ id, name: name.slice(0,20), group:'person', val:c.kategoriziran?12:4, color:c.reprezentativac?'#f59e0b':'#ec4899', _data:c });
|
||
const savezId = nodeMap[c.savez_id] || (nodes.length > 1 ? 'savez_' + saveziArr[0]?.id : null);
|
||
if (savezId) links.push({ source: savezId, target: id, color: 'rgba(236,72,153,0.1)' });
|
||
});
|
||
|
||
} else {
|
||
// Geo: Gradovi → Klubovi
|
||
const gradMap = {};
|
||
klubArr.forEach(k => { const g = k.grad||'Ostalo'; (gradMap[g]||(gradMap[g]=[])).push(k); });
|
||
Object.keys(gradMap).slice(0, 30).forEach(g => {
|
||
const id = 'grad_' + g;
|
||
nodes.push({ id, name: g, group:'savez', val:18, color:'#a78bfa' });
|
||
gradMap[g].slice(0, 20).forEach(k => {
|
||
const kid = 'klub_' + k.id;
|
||
nodes.push({ id:kid, name:(k.naziv||'').slice(0,22), group:'klub', val:k.nositelj_kvalitete?10:4, color:k.nositelj_kvalitete?'#f59e0b':'#22c55e', _data:k });
|
||
links.push({ source: id, target: kid, color:'rgba(167,139,250,0.15)' });
|
||
});
|
||
});
|
||
}
|
||
|
||
if (info) info.textContent = `${nodes.length} čvorova · ${links.length} veza`;
|
||
|
||
// Cleanup previous
|
||
if (_g3d) { try { _g3d._destructor(); } catch {} }
|
||
const container = document.getElementById('g3d-container');
|
||
if (!container) return;
|
||
container.innerHTML = '';
|
||
|
||
// Init 3D force graph
|
||
const ForceGraph3D = window.ForceGraph3D || window['3d-force-graph']?.default || window['ForceGraph3D'];
|
||
if (!ForceGraph3D) {
|
||
container.innerHTML = '<div style="color:#ef4444;padding:20px">3D knjižnica nije učitana. Refreshaj stranicu.</div>';
|
||
return;
|
||
}
|
||
|
||
const graph = ForceGraph3D()(container)
|
||
.width(container.clientWidth || 800)
|
||
.height(container.clientHeight || 550)
|
||
.graphData({ nodes, links })
|
||
.backgroundColor('#0d0d0d')
|
||
.nodeLabel(n => `<div style="background:rgba(5,8,16,0.92);border:1px solid rgba(0,212,255,0.25);border-radius:6px;padding:5px 10px;font-size:11px;color:#e2e8f0"><b style="color:${n.color}">${n.name}</b></div>`)
|
||
.nodeColor(n => n.color)
|
||
.nodeVal(n => (n.val || 5) * 3)
|
||
.nodeOpacity(0.9)
|
||
.nodeResolution(16)
|
||
.linkColor(l => l.color || 'rgba(255,255,255,0.05)')
|
||
.linkWidth(0.2)
|
||
.linkOpacity(0.5)
|
||
.onNodeClick(n => {
|
||
if (n._data?.id) {
|
||
if (n.group === 'klub') showKlub(n._data.id);
|
||
else if (n.group === 'person') navigate('sportas_' + n._data.id);
|
||
}
|
||
})
|
||
.onNodeHover(n => { container.style.cursor = n ? 'pointer' : 'grab'; });
|
||
|
||
graph.d3Force('charge')?.strength(-80);
|
||
graph.d3Force('link')?.distance(50);
|
||
_g3d = graph;
|
||
|
||
if (loader) loader.style.display = 'none';
|
||
}
|
||
|
||
|
||
async function initGraf3D() {
|
||
const sport = document.getElementById('gf-sport')?.value || '';
|
||
const mode = document.getElementById('gf-mode')?.value || 'savez';
|
||
const info = document.getElementById('gf-info');
|
||
if (info) info.textContent = 'Učitavam podatke...';
|
||
|
||
// Fetch data
|
||
const [savezi, klubovi] = await Promise.all([
|
||
api('/api/savezi').catch(()=>[]),
|
||
api('/api/klubovi' + (sport ? '?sport='+encodeURIComponent(sport) : '')).catch(()=>[])
|
||
]);
|
||
|
||
const klubArr = (klubovi.rows || klubovi || []);
|
||
const saveziArr = (savezi.rows || savezi || []);
|
||
|
||
if (info) info.textContent = `${saveziArr.length} saveza · ${klubArr.length} klubova`;
|
||
|
||
// Build graph nodes + edges
|
||
const nodes = [];
|
||
const edges = [];
|
||
const nodeMap = {};
|
||
|
||
if (mode === 'savez') {
|
||
// Center: PGŽ Sport
|
||
nodes.push({ id: 'pgz', label: 'PGŽ Sport', type: 'root', size: 3.0, color: 0xf59e0b, x:0, y:0, z:0 });
|
||
nodeMap['pgz'] = nodes.length - 1;
|
||
|
||
// Savezi kao planetarni ring
|
||
const sCount = Math.min(saveziArr.length, 30);
|
||
saveziArr.slice(0, sCount).forEach((s, i) => {
|
||
const angle = (i / sCount) * Math.PI * 2;
|
||
const r = 120 + Math.random() * 40;
|
||
const y = (Math.random() - 0.5) * 60;
|
||
const id = 's_' + s.id;
|
||
nodes.push({
|
||
id, label: (s.naziv||'Savez').replace('savez PGŽ','').replace('Savez PGŽ','').trim().slice(0,22),
|
||
type: 'savez', size: 1.4, color: 0x3b82f6,
|
||
x: Math.cos(angle)*r, y, z: Math.sin(angle)*r,
|
||
data: s
|
||
});
|
||
nodeMap[id] = nodes.length - 1;
|
||
edges.push({ from: 'pgz', to: id, color: 0x1e40af });
|
||
});
|
||
|
||
// Klubovi kao sateliti saveza
|
||
const colors3d = [0x22c55e, 0x10b981, 0x34d399, 0x6ee7b7];
|
||
klubArr.slice(0, 200).forEach((k, i) => {
|
||
const sid = 's_' + k.savez_id;
|
||
if (!nodeMap[sid] && !nodeMap['pgz']) return;
|
||
const parent = nodeMap[sid] !== undefined ? nodes[nodeMap[sid]] : nodes[0];
|
||
const angle = Math.random() * Math.PI * 2;
|
||
const r2 = 28 + Math.random() * 22;
|
||
const ky = parent.y + (Math.random()-0.5)*30;
|
||
const id = 'k_' + k.id;
|
||
nodes.push({
|
||
id, label: (k.naziv||'Klub').slice(0,18),
|
||
type: 'klub', size: 0.7 + (k.nositelj_kvalitete ? 0.4 : 0),
|
||
color: k.nositelj_kvalitete ? 0xf59e0b : colors3d[i%colors3d.length],
|
||
x: parent.x + Math.cos(angle)*r2, y: ky, z: parent.z + Math.sin(angle)*r2,
|
||
data: k
|
||
});
|
||
nodeMap[id] = nodes.length - 1;
|
||
edges.push({ from: sid !== 's_undefined' ? sid : 'pgz', to: id, color: 0x1d4d20 });
|
||
});
|
||
|
||
} else if (mode === 'sport') {
|
||
// Group by sport
|
||
const sportGroups = {};
|
||
klubArr.forEach(k => { const sp = k.sport||'ostalo'; (sportGroups[sp]||(sportGroups[sp]=[])).push(k); });
|
||
const sportColors = { 'nogomet':0xef4444,'košarka':0xf97316,'odbojka':0xa78bfa,
|
||
'rukomet':0x22c55e,'bočanje':0x06b6d4,'skijanje':0x93c5fd,'tenis':0xfbbf24,'ostalo':0x64748b };
|
||
const sports = Object.keys(sportGroups);
|
||
nodes.push({ id:'pgz',label:'PGŽ Sport',type:'root',size:3.0,color:0xf59e0b,x:0,y:0,z:0 });
|
||
sports.slice(0,20).forEach((sp,si) => {
|
||
const angle=(si/sports.length)*Math.PI*2;
|
||
const r=100;
|
||
const col = sportColors[sp] || 0x64748b;
|
||
const spid='sp_'+sp;
|
||
nodes.push({ id:spid, label:sp, type:'sport_cat', size:1.8, color:col,
|
||
x:Math.cos(angle)*r, y:(Math.random()-0.5)*40, z:Math.sin(angle)*r });
|
||
edges.push({ from:'pgz', to:spid, color:col });
|
||
const sx=Math.cos(angle)*r, sz=Math.sin(angle)*r;
|
||
sportGroups[sp].slice(0,15).forEach((k,ki) => {
|
||
const a2=Math.random()*Math.PI*2, r2=25+Math.random()*20;
|
||
nodes.push({ id:'k_'+k.id, label:(k.naziv||'').slice(0,16), type:'klub', size:0.65,
|
||
color:col, x:sx+Math.cos(a2)*r2, y:(Math.random()-0.5)*30, z:sz+Math.sin(a2)*r2, data:k });
|
||
edges.push({ from:spid, to:'k_'+k.id, color:col });
|
||
});
|
||
});
|
||
|
||
} else {
|
||
// Geografija
|
||
const geoMap = {};
|
||
klubArr.forEach(k => { const g=k.grad||'Ostalo'; (geoMap[g]||(geoMap[g]=[])).push(k); });
|
||
const geoPositions = {
|
||
'Rijeka':{x:0,z:0},'Opatija':{x:80,z:-30},'Krk':{x:120,z:60},
|
||
'Crikvenica':{x:-60,z:80},'Delnice':{x:-90,z:-60},'Čabar':{x:-130,z:-80},
|
||
'Mali Lošinj':{x:150,z:100},'Rab':{x:130,z:130},'Senj':{x:-100,z:120}
|
||
};
|
||
nodes.push({ id:'pgz',label:'PGŽ',type:'root',size:3.0,color:0xf59e0b,x:0,y:50,z:0 });
|
||
const geoKeys = Object.keys(geoMap);
|
||
geoKeys.slice(0,25).forEach((g,gi) => {
|
||
const pos = geoPositions[g] || { x:(Math.random()-0.5)*200, z:(Math.random()-0.5)*200 };
|
||
const gid = 'g_'+g;
|
||
nodes.push({ id:gid, label:g, type:'grad', size:1.5, color:0x06b6d4,
|
||
x:pos.x, y:0, z:pos.z });
|
||
edges.push({ from:'pgz', to:gid, color:0x0891b2 });
|
||
geoMap[g].slice(0,12).forEach((k,ki) => {
|
||
const a=Math.random()*Math.PI*2, r=20+Math.random()*18;
|
||
nodes.push({ id:'k_'+k.id, label:(k.naziv||'').slice(0,16), type:'klub', size:0.65,
|
||
color:0x22c55e, x:pos.x+Math.cos(a)*r, y:-10+(Math.random()-0.5)*20, z:pos.z+Math.sin(a)*r, data:k });
|
||
edges.push({ from:gid, to:'k_'+k.id, color:0x166534 });
|
||
});
|
||
});
|
||
}
|
||
|
||
render3DGraph(nodes, edges, mode);
|
||
}
|
||
|
||
function render3DGraph(nodes, edges, mode) {
|
||
if (!window.THREE) { console.error('Three.js not loaded'); return; }
|
||
const THREE = window.THREE;
|
||
const canvas = document.getElementById('graf3d-canvas');
|
||
const container = document.getElementById('graf3d-container');
|
||
const tooltip = document.getElementById('graf3d-tooltip');
|
||
const legend = document.getElementById('graf3d-legend');
|
||
|
||
if (!canvas || !container) return;
|
||
const W = container.clientWidth, H = container.clientHeight;
|
||
|
||
// Cleanup previous
|
||
if (window._sportGraf3D) {
|
||
window._sportGraf3D.renderer.dispose();
|
||
cancelAnimationFrame(window._sportGraf3D.raf);
|
||
}
|
||
|
||
const renderer = new THREE.WebGLRenderer({ canvas, antialias:true, alpha:true });
|
||
renderer.setSize(W, H);
|
||
renderer.setPixelRatio(Math.min(window.devicePixelRatio,2));
|
||
renderer.setClearColor(0x080912, 1);
|
||
|
||
const scene = new THREE.Scene();
|
||
scene.fog = new THREE.FogExp2(0x080912, 0.0015);
|
||
|
||
const camera = new THREE.PerspectiveCamera(60, W/H, 0.1, 2000);
|
||
camera.position.set(0, 120, 280);
|
||
camera.lookAt(0,0,0);
|
||
|
||
// Ambient + directional light
|
||
scene.add(new THREE.AmbientLight(0x111122, 2));
|
||
const dlight = new THREE.DirectionalLight(0x4466ff, 1);
|
||
dlight.position.set(100, 200, 100);
|
||
scene.add(dlight);
|
||
const plight = new THREE.PointLight(0xf59e0b, 2, 300);
|
||
plight.position.set(0,50,0);
|
||
scene.add(plight);
|
||
|
||
// Star field
|
||
const starGeo = new THREE.BufferGeometry();
|
||
const starPos = [];
|
||
for (let i=0; i<2000; i++) {
|
||
starPos.push((Math.random()-0.5)*2000,(Math.random()-0.5)*2000,(Math.random()-0.5)*2000);
|
||
}
|
||
starGeo.setAttribute('position', new THREE.Float32BufferAttribute(starPos,3));
|
||
scene.add(new THREE.Points(starGeo, new THREE.PointsMaterial({color:0x334155,size:0.8})));
|
||
|
||
// Node meshes
|
||
const nodeMeshes = [];
|
||
const nodeIndex = {};
|
||
const typeGeo = {
|
||
root: new THREE.SphereGeometry(2.8, 32, 32),
|
||
savez: new THREE.SphereGeometry(1.6, 20, 20),
|
||
sport_cat: new THREE.OctahedronGeometry(1.8, 0),
|
||
grad: new THREE.DodecahedronGeometry(1.4, 0),
|
||
klub: new THREE.SphereGeometry(0.75, 12, 12),
|
||
};
|
||
|
||
nodes.forEach((n, i) => {
|
||
const geo = typeGeo[n.type] || typeGeo.klub;
|
||
const mat = new THREE.MeshPhongMaterial({
|
||
color: n.color, emissive: n.color, emissiveIntensity: 0.3,
|
||
shininess: 80, transparent: true, opacity: 0.9
|
||
});
|
||
const mesh = new THREE.Mesh(geo, mat);
|
||
mesh.position.set(n.x||0, n.y||0, n.z||0);
|
||
mesh.userData = { node: n, idx: i };
|
||
scene.add(mesh);
|
||
nodeMeshes.push(mesh);
|
||
nodeIndex[n.id] = mesh;
|
||
});
|
||
|
||
// Edge lines
|
||
const edgeMat = new THREE.LineBasicMaterial({ vertexColors: true, transparent: true, opacity: 0.25 });
|
||
const edgeGeo = new THREE.BufferGeometry();
|
||
const edgePos = [], edgeColors = [];
|
||
edges.forEach(e => {
|
||
const fm = nodeIndex[e.from], tm = nodeIndex[e.to];
|
||
if (!fm || !tm) return;
|
||
const c = new THREE.Color(e.color||0x334155);
|
||
edgePos.push(fm.position.x,fm.position.y,fm.position.z);
|
||
edgePos.push(tm.position.x,tm.position.y,tm.position.z);
|
||
edgeColors.push(c.r,c.g,c.b, c.r,c.g,c.b);
|
||
});
|
||
edgeGeo.setAttribute('position', new THREE.Float32BufferAttribute(edgePos,3));
|
||
edgeGeo.setAttribute('color', new THREE.Float32BufferAttribute(edgeColors,3));
|
||
scene.add(new THREE.LineSegments(edgeGeo, edgeMat));
|
||
|
||
// Labels (sprites)
|
||
function makeLabel(text, color) {
|
||
const cv = document.createElement('canvas');
|
||
cv.width=256; cv.height=48;
|
||
const ctx=cv.getContext('2d');
|
||
ctx.font='bold 20px system-ui';
|
||
ctx.fillStyle = '#'+color.toString(16).padStart(6,'0');
|
||
ctx.fillText(text, 4, 32);
|
||
const tex = new THREE.CanvasTexture(cv);
|
||
const mat = new THREE.SpriteMaterial({map:tex,transparent:true,opacity:0.85});
|
||
const s = new THREE.Sprite(mat);
|
||
s.scale.set(30,6,1);
|
||
return s;
|
||
}
|
||
// Only label large nodes
|
||
nodes.filter(n=>n.type!=='klub').forEach(n => {
|
||
const lbl = makeLabel(n.label, n.color);
|
||
lbl.position.set(n.x||0, (n.y||0)+(n.size||1)*3+4, n.z||0);
|
||
scene.add(lbl);
|
||
});
|
||
|
||
// Legend
|
||
const legendItems = {
|
||
'savez': { color:'#3b82f6', label:'Savez' },
|
||
'klub': { color:'#22c55e', label:'Klub' },
|
||
'★ qual':{ color:'#f59e0b', label:'Nositelj kvalitete' },
|
||
};
|
||
if (legend) {
|
||
legend.innerHTML = Object.entries(legendItems).map(([k,v])=>
|
||
`<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px">
|
||
<div style="width:10px;height:10px;border-radius:50%;background:${v.color}"></div>
|
||
<span>${v.label}</span></div>`
|
||
).join('');
|
||
}
|
||
|
||
// Camera orbit controls (simple)
|
||
let isDragging=false, prevMouse={x:0,y:0};
|
||
let theta=0, phi=0.4, radius=280;
|
||
canvas.addEventListener('mousedown', e=>{ isDragging=true; prevMouse={x:e.clientX,y:e.clientY}; });
|
||
canvas.addEventListener('mouseup', ()=>isDragging=false);
|
||
canvas.addEventListener('mousemove', e=>{
|
||
if (!isDragging) {
|
||
// Tooltip
|
||
const rect=canvas.getBoundingClientRect();
|
||
const mx=((e.clientX-rect.left)/rect.width)*2-1;
|
||
const my=-((e.clientY-rect.top)/rect.height)*2+1;
|
||
const raycaster=new THREE.Raycaster();
|
||
raycaster.setFromCamera({x:mx,y:my},camera);
|
||
const hits=raycaster.intersectObjects(nodeMeshes);
|
||
if (hits.length && hits[0].object.userData.node) {
|
||
const n=hits[0].object.userData.node;
|
||
tooltip.style.display='block';
|
||
tooltip.style.left=(e.clientX-rect.left+16)+'px';
|
||
tooltip.style.top=(e.clientY-rect.top-40)+'px';
|
||
const d=n.data||{};
|
||
tooltip.innerHTML = `<div style="font-weight:700;color:#f59e0b;margin-bottom:4px">${n.label}</div>`+
|
||
`<div style="color:#64748b;font-size:10px;text-transform:uppercase;margin-bottom:6px">${n.type}</div>`+
|
||
(d.sport?`<div>Sport: <b>${d.sport}</b></div>`:'')+(d.grad?`<div>Grad: <b>${d.grad}</b></div>`:'')+
|
||
(d.broj_clanova?`<div>Članova: <b>${d.broj_clanova}</b></div>`:'')+
|
||
(d.nositelj_kvalitete?`<div style="color:#f59e0b">★ Nositelj kvalitete</div>`:'');
|
||
canvas.style.cursor='pointer';
|
||
} else {
|
||
tooltip.style.display='none';
|
||
canvas.style.cursor='grab';
|
||
}
|
||
return;
|
||
}
|
||
const dx=e.clientX-prevMouse.x, dy=e.clientY-prevMouse.y;
|
||
theta -= dx * 0.008;
|
||
phi = Math.max(0.1, Math.min(Math.PI-0.1, phi - dy*0.006));
|
||
prevMouse={x:e.clientX,y:e.clientY};
|
||
});
|
||
canvas.addEventListener('click', e => {
|
||
const rect=canvas.getBoundingClientRect();
|
||
const mx=((e.clientX-rect.left)/rect.width)*2-1;
|
||
const my=-((e.clientY-rect.top)/rect.height)*2+1;
|
||
const raycaster=new THREE.Raycaster();
|
||
raycaster.setFromCamera({x:mx,y:my},camera);
|
||
const hits=raycaster.intersectObjects(nodeMeshes);
|
||
if (hits.length) {
|
||
const n=hits[0].object.userData.node;
|
||
if (n.type==='klub' && n.data?.id) {
|
||
navigate('klubovi');
|
||
setTimeout(()=>{ window._drillClubId = n.data.id; window._drillClubName=n.label; }, 300);
|
||
}
|
||
}
|
||
});
|
||
canvas.addEventListener('wheel', e=>{ radius=Math.max(80,Math.min(600,radius+e.deltaY*0.3)); });
|
||
canvas.style.cursor='grab';
|
||
|
||
// Animation loop
|
||
let t=0;
|
||
function animate() {
|
||
const raf=requestAnimationFrame(animate);
|
||
window._sportGraf3D = { renderer, raf };
|
||
t+=0.005;
|
||
// Auto-rotate
|
||
if (!isDragging) theta += 0.002;
|
||
camera.position.x = radius * Math.sin(phi) * Math.sin(theta);
|
||
camera.position.y = radius * Math.cos(phi);
|
||
camera.position.z = radius * Math.sin(phi) * Math.cos(theta);
|
||
camera.lookAt(0,0,0);
|
||
// Pulse root node
|
||
nodeMeshes[0] && (nodeMeshes[0].material.emissiveIntensity = 0.3 + Math.sin(t*3)*0.2);
|
||
renderer.render(scene, camera);
|
||
}
|
||
animate();
|
||
window._sportGraf3D = { renderer, raf: 0 };
|
||
}
|
||
|
||
|
||
async function pageSavezi() {
|
||
setTopbar('Organizacija', 'Županijski savezi');
|
||
const c = document.getElementById('content');
|
||
const q = document.getElementById('q-savezi')?.value || '';
|
||
try {
|
||
const razina = state.filters.savez_razina !== undefined ? state.filters.savez_razina : 'zupanijski'; const d = await api('/api/savezi?'+(q?`q=${encodeURIComponent(q)}&`:'')+(razina?`razina=${encodeURIComponent(razina)}`:'')+getSort('savezi'));
|
||
c.innerHTML = `
|
||
<div class="toolbar">
|
||
<input class="inp flex" id="q-savezi" placeholder="🔍 Pretraga..." value="${q}" oninput="dbS()">
|
||
<select onchange="state.filters.savez_razina=this.value;render()">
|
||
<option value="zupanijski" ${(state.filters.savez_razina||'zupanijski')==='zupanijski'?'selected':''}>Županijski (PGŽ)</option>
|
||
<option value="gradski" ${state.filters.savez_razina==='gradski'?'selected':''}>Gradski</option>
|
||
<option value="opcinski" ${state.filters.savez_razina==='opcinski'?'selected':''}>Općinski</option>
|
||
<option value="strukovni" ${state.filters.savez_razina==='strukovni'?'selected':''}>Strukovni</option>
|
||
<option value="nacional" ${state.filters.savez_razina==='nacional'?'selected':''}>Nacionalni</option>
|
||
<option value="" ${state.filters.savez_razina===''?'selected':''}>Sve razine</option>
|
||
</select>
|
||
<span style="color:var(--text-3);font-size:11px">${d.count} saveza</span>
|
||
</div>
|
||
<div class="tbl-wrap"><table>
|
||
<thead><tr>${tableHeader([{key:'naziv',label:'Naziv'},{key:'sport',label:'Sport'},{key:'godina',label:'Osn.'},{key:'klubova',label:'Klub.'},{key:'klubova',label:'Reg.',sort:false},{key:'klubova',label:'Tren.',sort:false},{key:'klubova',label:'Repr.',sort:false}], 'savezi')}</tr></thead>
|
||
<tbody>${d.rows.map(r=>`<tr onclick="showSavez(${r.id})">
|
||
<td><b>${r.naziv}</b></td>
|
||
<td class="dim">${r.sport||'–'}</td>
|
||
<td class="num">${r.godina_osnutka||'–'}</td>
|
||
<td class="num">${r.broj_klubova||0}</td>
|
||
<td class="num">${fmt(r.reg_2024)}</td>
|
||
<td class="num">${fmt(r.treneri_2024)}</td>
|
||
<td class="num">${fmt(r.repr_2024)}</td>
|
||
</tr>`).join('')}</tbody>
|
||
</table></div>
|
||
`;
|
||
} catch(e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
|
||
}
|
||
const dbS = debounce(pageSavezi, 300);
|
||
const dbStat = debounce(pageStatistika, 300);
|
||
async function showSavez(id) {
|
||
try {
|
||
const d = await api('/api/savezi/'+id);
|
||
openDrawer(`<div class="dr-h">
|
||
<div><div class="bc">SAVEZ · #${d.id}</div><h3>${d.naziv}</h3>
|
||
<div style="color:var(--text-3);font-size:12px;margin-top:3px">${d.sport||'–'} · osn. ${d.godina_osnutka||'–'}</div></div>
|
||
<button class="dr-x" onclick="closeDrawer()">✕</button></div>
|
||
<div class="dr-b"><dl>
|
||
<dt>Email</dt><dd>${d.email||'–'}</dd>
|
||
<dt>Web</dt><dd>${d.web?`<a href="${d.web}" target="_blank">${d.web}</a>`:'–'}</dd>
|
||
<dt>Klubova</dt><dd>${d.klubovi.length}</dd>
|
||
<dt>Manifestacija</dt><dd>${d.manifestacije.length}</dd>
|
||
</dl>
|
||
<h4>Statistika kroz godine</h4>
|
||
<table class="sub-tbl"><thead><tr><th>God</th><th style="text-align:right">Klub.</th><th style="text-align:right">Reg.</th><th style="text-align:right">Tren.</th><th style="text-align:right">Repr.</th></tr></thead>
|
||
<tbody>${d.statistika.map(s=>`<tr><td><b>${s.godina}</b></td><td class="num">${s.klubova_clanica}</td><td class="num">${s.registriranih}</td><td class="num">${s.trenera}</td><td class="num">${s.reprezentativaca}</td></tr>`).join('')}</tbody></table>
|
||
${d.klubovi.length?`<h4>Klubovi-članice (${d.klubovi.length})</h4>
|
||
<table class="sub-tbl"><tbody>${d.klubovi.slice(0,15).map(k=>{const mq=encodeURIComponent((k.naziv||'')+' '+(k.grad||'')+' Hrvatska');return `<tr style="cursor:pointer" onclick="closeDrawer();showKlub(${k.id})"><td><b style="color:var(--accent)">${k.naziv}</b></td><td><span class="bdg muted">${k.razina||'–'}</span></td><td class="dim" style="text-align:right"><a href="https://www.google.com/maps/search/?api=1&query=${mq}" target="_blank" onclick="event.stopPropagation()" class="muted">📍 ${k.grad||'–'}</a></td></tr>`}).join('')}</tbody></table>`:''}
|
||
</div>`);
|
||
} catch(e) { alert(e.message); }
|
||
}
|
||
|
||
async function pageKlubovi() {
|
||
setTopbar('Klubovi ERP', 'Upravljanje klubovima · '+document.getElementById('q-klub')?.value||'');
|
||
const c = document.getElementById('content');
|
||
const q = document.getElementById('q-klub')?.value||'';
|
||
const fnos = state.filters.nositelj||'';
|
||
const freg = state.filters.region||'';
|
||
const fsport = state.filters.sport||'';
|
||
const faktivan = state.filters.aktivan||'true';
|
||
try {
|
||
const [d, filterOpts] = await Promise.all([
|
||
api('/api/klubovi?' + new URLSearchParams(Object.fromEntries([
|
||
q?['q',q]:null, fnos?['nositelj',fnos]:null,
|
||
freg?['region',freg]:null, fsport?['sport',fsport]:null,
|
||
faktivan?['aktivan',faktivan]:null
|
||
].filter(Boolean))).toString() + getSort('klubovi')),
|
||
api('/api/v2/analytics/filter-options').catch(()=>({}))
|
||
]);
|
||
const sportOpts = (filterOpts.sportovi||['nogomet','košarka','odbojka','rukomet','bočanje','skijanje','jedrenje','atletika','tenis','šah','plivanje','veslanje']).slice(0,20);
|
||
c.innerHTML = `
|
||
<div class="toolbar">
|
||
<input class="inp flex" id="q-klub" placeholder="🔍 Pretraga..." value="${q}" oninput="dbK()">
|
||
<select onchange="state.filters.sport=(this.value||\"\");pageKlubovi()\"><option value=\"\">Svi sportovi</option><option value=\"nogomet\">⚽ Nogomet</option><option value=\"košarka\">🏀 Košarka</option><option value=\"odbojka\">🏐 Odbojka</option><option value=\"rukomet\">🤾 Rukomet</option><option value=\"bočanje\">🎯 Bočanje</option><option value=\"skijanje\">⛷ Skijanje</option><option value=\"tenis\">🎾 Tenis</option><option value=\"vaterpolo\">🤽 Vaterpolo</option><option value=\"atletika\">🏃 Atletika</option><option value=\"jedrenje\">⛵ Jedrenje</option></select>\n <button onclick=\"exportKlubovi()\" style=\"background:#1e293b;color:#94a3b8;border:1px solid #334;padding:5px 10px;border-radius:6px;cursor:pointer;font-size:11px\">↓ CSV</button>\n <select onchange=\"state.filters.nositelj=this.value;render()">
|
||
<option value="">Svi klubovi</option>
|
||
<option value="true" ${fnos==='true'?'selected':''}>⭐ Nositelji</option>
|
||
<option value="false" ${fnos==='false'?'selected':''}>Bez nositelja</option>
|
||
</select>
|
||
<select onchange="state.filters.region=this.value;render()">
|
||
<option value="">Sve regije</option>
|
||
${['Rijeka','Liburnija','Primorje','Gorski kotar','Otoci','Zaleđe'].map(r=>`<option value="${r}" ${freg===r?'selected':''}>${r}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="tbl-wrap"><table>
|
||
<thead><tr>${tableHeader([{key:'klub',label:'Klub'},{key:'sport',label:'Sport'},{key:'grad',label:'Grad',sort:false},{key:'predsjednik',label:'Predsjednik',sort:false},{key:'oib',label:'OIB',sort:false},{key:'broj_clanova',label:'Čl.'},{key:'completeness',label:'Kompletnost',sort:false},{key:'akcije',label:'',sort:false}], 'klubovi')}</tr></thead>
|
||
<tbody>${d.rows.map(r=>{
|
||
const enrichDots = [
|
||
r.ima_oib ? '<span title="OIB" style="color:var(--ok)">●</span>' : '<span title="Nema OIB" style="color:var(--text-dim);opacity:0.3">●</span>',
|
||
r.ima_predsjednika ? '<span title="Predsjednik" style="color:var(--ok)">●</span>' : '<span title="Nema predsjednika" style="color:var(--text-dim);opacity:0.3">●</span>',
|
||
r.ima_ciljeve ? '<span title="Ciljevi" style="color:var(--accent)">●</span>' : '<span title="Bez ciljeva" style="color:var(--text-dim);opacity:0.3">●</span>',
|
||
r.ima_sjediste ? '<span title="Sjedište" style="color:var(--accent)">●</span>' : '<span title="Bez sjedišta" style="color:var(--text-dim);opacity:0.3">●</span>',
|
||
].join(' ');
|
||
return `<tr onclick="showKlub(${r.id})">
|
||
<td><b>${r.klub}</b>${r.razina?` <span class="bdg muted" style="font-size:9px">${r.razina}</span>`:''}</td>
|
||
<td class="dim">${r.sport||'–'}</td>
|
||
<td>${r.predsjednik||'<span class="dim">–</span>'}</td>
|
||
<td class="mono" style="font-size:11px">${r.oib||'<span class="dim">–</span>'}</td>
|
||
<td class="dim" style="font-size:11px">${r.savez||'–'}</td>
|
||
<td class="num">${r.broj_clanova||0}</td>
|
||
<td style="white-space:nowrap;font-size:14px">${enrichDots}${r.nositelj_kvalitete?' <span class="bdg gold" style="font-size:9px">⭐</span>':''}</td>
|
||
</tr>`;
|
||
}).join('')}</tbody>
|
||
</table></div>
|
||
<div style="font-size:10px;color:var(--text-dim);margin-top:8px;padding:0 8px">
|
||
Status: <span style="color:var(--ok)">●</span> OIB · <span style="color:var(--ok)">●</span> predsjednik · <span style="color:var(--accent)">●</span> ciljevi · <span style="color:var(--accent)">●</span> sjedište
|
||
</div>
|
||
`;
|
||
} catch(e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
|
||
}
|
||
|
||
// CSV export za klubove
|
||
function exportKlubovi() {
|
||
api('/api/klubovi?limit=5000').then(d => {
|
||
const rows = d.rows || d || [];
|
||
const headers = ['ID','Naziv','Sport','Grad','OIB','Predsjednik','Email','Web','Broj_clanova','Nositelj','Aktivan'];
|
||
const csv = [headers.join(','), ...rows.map(r => [
|
||
r.id, '"'+(r.klub||'').replace(/"/g,'""')+'"', r.sport||'', r.grad||'',
|
||
r.oib||'', '"'+(r.predsjednik||'').replace(/"/g,'""')+'"',
|
||
r.email||'', r.web||'', r.broj_clanova||0,
|
||
r.nositelj_kvalitete?'DA':'NE', r.aktivan?'DA':'NE'
|
||
].join(','))].join('
|
||
');
|
||
const a = document.createElement('a');
|
||
a.href = 'data:text/csv;charset=utf-8,' + encodeURIComponent(csv);
|
||
a.download = 'pgz_klubovi_' + new Date().toISOString().slice(0,10) + '.csv';
|
||
a.click();
|
||
});
|
||
}
|
||
|
||
// Data quality dashboard
|
||
async function pageDataQuality() {
|
||
setTopbar('Kvaliteta podataka', 'Kompletnost i enrichment status');
|
||
const c = document.getElementById('content');
|
||
c.innerHTML = '<div class="loader">Analiziram podatke...</div>';
|
||
const [quality, gap] = await Promise.all([
|
||
api('/api/v2/audit/data-quality').catch(()=>({})),
|
||
api('/api/v2/pgz/enrichment-gap').catch(()=>({rows:[]}))
|
||
]);
|
||
const fields = quality.fields || [];
|
||
const totalKlubova = quality.total_klubova || 0;
|
||
c.innerHTML = `
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;margin-bottom:20px">
|
||
${(fields||[]).map(f=>{
|
||
const pct=Math.round((f.has||0)/(totalKlubova||1)*100);
|
||
const col=pct>=80?'#22c55e':pct>=50?'#f59e0b':'#ef4444';
|
||
return `<div style="background:#0d1117;border:1px solid #1e293b;border-radius:10px;padding:14px">
|
||
<div style="font-size:11px;color:#64748b;margin-bottom:6px;text-transform:uppercase">${f.field||f.label}</div>
|
||
<div style="font-size:24px;font-weight:700;color:${col}">${pct}%</div>
|
||
<div style="margin-top:8px;background:#1e293b;height:4px;border-radius:2px">
|
||
<div style="width:${pct}%;height:100%;background:${col};border-radius:2px"></div>
|
||
</div>
|
||
<div style="font-size:10px;color:#475569;margin-top:4px">${f.has||0} / ${totalKlubova} klubova</div>
|
||
</div>`;
|
||
}).join('')}
|
||
</div>
|
||
<h3 style="color:#94a3b8;margin-bottom:12px">Klubovi bez ključnih podataka</h3>
|
||
<div class="tbl-wrap"><table>
|
||
<thead><tr><th>Klub</th><th>Sport</th><th>Grad</th><th>Nedostaje</th><th>Akcija</th></tr></thead>
|
||
<tbody>${((gap.rows||[]).slice(0,50)).map(r=>`
|
||
<tr onclick="showKlub(${r.id})" style="cursor:pointer">
|
||
<td><b>${r.naziv||r.klub}</b></td>
|
||
<td class="dim">${r.sport||'–'}</td>
|
||
<td class="dim">${r.grad||'–'}</td>
|
||
<td style="color:#ef4444;font-size:11px">${r.nedostaje||r.missing_fields||'–'}</td>
|
||
<td><button onclick="event.stopPropagation();enrichKlub(${r.id})" style="background:#0f172a;border:1px solid #f59e0b22;color:#f59e0b;padding:2px 8px;border-radius:4px;cursor:pointer;font-size:10px">⚡ AI Enrichment</button></td>
|
||
</tr>`).join('')}
|
||
</tbody>
|
||
</table></div>`;
|
||
}
|
||
|
||
async function enrichKlub(id) {
|
||
const r = await api('/api/v2/enrich/klub-web?klub_id='+id).catch(e=>({error:e.message}));
|
||
if (r.error) alert('Enrichment: ' + r.error);
|
||
else { showKlub(id); }
|
||
}
|
||
|
||
// DMS — Document Management
|
||
async function pageDMS() {
|
||
setTopbar('DMS', 'Upravljanje dokumentima · OCR · Pretraga');
|
||
const c = document.getElementById('content');
|
||
const q = document.getElementById('dms-q')?.value||'';
|
||
const [docs, stats] = await Promise.all([
|
||
api('/api/v2/dokumenti/list?' + (q?'q='+encodeURIComponent(q):'')).catch(()=>({rows:[]})),
|
||
api('/api/v2/audit/coverage').catch(()=>({}))
|
||
]);
|
||
c.innerHTML = `
|
||
<div class="toolbar" style="margin-bottom:16px">
|
||
<input id="dms-q" class="inp" placeholder="🔍 Pretraži dokumente (full-text)..." value="${q}" oninput="dbDMS()" style="flex:1;max-width:400px">
|
||
<label style="background:#1e40af;color:#fff;padding:6px 14px;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600">
|
||
⬆ Upload dokument
|
||
<input type="file" style="display:none" onchange="uploadDok(this)" accept=".pdf,.doc,.docx,.jpg,.png">
|
||
</label>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:16px">
|
||
<div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px">
|
||
<div style="color:#64748b;font-size:10px;text-transform:uppercase">Ukupno dokumenata</div>
|
||
<div style="font-size:28px;font-weight:700;color:#e2e8f0">${stats.total_docs||'–'}</div>
|
||
</div>
|
||
<div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px">
|
||
<div style="color:#64748b;font-size:10px;text-transform:uppercase">OCR obrađeno</div>
|
||
<div style="font-size:28px;font-weight:700;color:#22c55e">${stats.ocr_done||'–'}</div>
|
||
</div>
|
||
<div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px">
|
||
<div style="color:#64748b;font-size:10px;text-transform:uppercase">Čeka OCR</div>
|
||
<div style="font-size:28px;font-weight:700;color:#f59e0b">${stats.ocr_pending||'–'}</div>
|
||
</div>
|
||
</div>
|
||
<div class="tbl-wrap"><table>
|
||
<thead><tr><th>Dokument</th><th>Tip</th><th>Klub</th><th>Datum</th><th>OCR</th><th>Akcije</th></tr></thead>
|
||
<tbody>${((docs.rows||docs||[]).slice(0,100)).map(d=>`
|
||
<tr>
|
||
<td><b style="color:#e2e8f0">${d.naziv||d.title||d.filename||'–'}</b></td>
|
||
<td><span class="bdg muted" style="font-size:10px">${d.tip||d.type||d.razina||'–'}</span></td>
|
||
<td class="dim" style="font-size:11px">${d.klub_naziv||d.klub||'–'}</td>
|
||
<td class="dim" style="font-size:11px">${d.datum||(d.created_at||'').slice(0,10)||'–'}</td>
|
||
<td style="text-align:center">${d.has_text||d.ocr_done?'<span style="color:#22c55e">✓</span>':'<span style="color:#334155">○</span>'}</td>
|
||
<td style="white-space:nowrap">
|
||
<button onclick="viewDok(${d.id||d.did})" style="background:#0f172a;border:1px solid #1e293b;color:#64748b;padding:2px 8px;border-radius:4px;cursor:pointer;font-size:10px;margin-right:4px">👁 Pregled</button>
|
||
${!d.has_text?`<button onclick="ocrDok(${d.id||d.did})" style="background:#0f172a;border:1px solid #f59e0b22;color:#f59e0b;padding:2px 8px;border-radius:4px;cursor:pointer;font-size:10px">🔤 OCR</button>`:''}
|
||
</td>
|
||
</tr>`).join('')}
|
||
</tbody>
|
||
</table></div>`;
|
||
}
|
||
const dbDMS = debounce(pageDMS, 350);
|
||
|
||
async function viewDok(id) {
|
||
const d = await api('/api/v2/dokumenti/'+id).catch(()=>null);
|
||
if (!d) return;
|
||
const modal = document.createElement('div');
|
||
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.85);z-index:1000;display:flex;align-items:center;justify-content:center';
|
||
modal.innerHTML = `<div style="background:#0d1117;border:1px solid #1e293b;border-radius:12px;padding:24px;max-width:700px;width:90%;max-height:80vh;overflow-y:auto">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
||
<h2 style="color:#e2e8f0;font-size:16px;margin:0">${d.naziv||d.title||'Dokument'}</h2>
|
||
<button onclick="this.closest('[style*=fixed]').remove()" style="background:none;border:none;color:#64748b;cursor:pointer;font-size:20px">×</button>
|
||
</div>
|
||
${d.text_content?`<pre style="color:#94a3b8;font-size:12px;white-space:pre-wrap;line-height:1.6">${d.text_content.slice(0,3000)}</pre>`:'<div style="color:#475569">Tekst nije dostupan. Pokrenite OCR.</div>'}
|
||
${d.pdf_url?`<a href="${d.pdf_url}" target="_blank" style="display:inline-block;margin-top:12px;color:#3b82f6;font-size:12px">↗ Otvori PDF</a>`:''}
|
||
</div>`;
|
||
modal.onclick = e => { if(e.target===modal) modal.remove(); };
|
||
document.body.appendChild(modal);
|
||
}
|
||
|
||
async function ocrDok(id) {
|
||
const btn = event.target;
|
||
btn.textContent = '⏳ OCR...'; btn.disabled=true;
|
||
const r = await api('/api/ai/ocr-prilog?doc_id='+id, {method:'POST'}).catch(e=>({error:e.message}));
|
||
if (r.error) { btn.textContent='⚠ Greška'; btn.style.color='#ef4444'; }
|
||
else { btn.textContent='✓ Gotovo'; btn.style.color='#22c55e'; }
|
||
}
|
||
|
||
async function uploadDok(input) {
|
||
const file = input.files[0]; if(!file) return;
|
||
const fd = new FormData(); fd.append('file', file);
|
||
const r = await fetch('/api/v2/dms/upload', {method:'POST', body:fd}).then(r=>r.json()).catch(e=>({error:e.message}));
|
||
if (r.error) alert('Upload greška: '+r.error);
|
||
else { pageDMS(); }
|
||
}
|
||
|
||
const dbK = debounce(pageKlubovi, 300);
|
||
async function showKlub(id) {
|
||
navigate('klub_detail_' + id);
|
||
const c = document.getElementById('content');
|
||
c.innerHTML = '<div class="loader">Učitavam klub...</div>';
|
||
try {
|
||
const [d, clanovi, docs, nat] = await Promise.all([
|
||
api('/api/v2/klub/' + id + '/dashboard'),
|
||
api('/api/v2/klubovi/' + id + '/clanovi').catch(()=>({rows:[]})),
|
||
api('/api/v2/dokumenti/list?klub_id=' + id).catch(()=>({rows:[]})),
|
||
api('/api/v2/klub/' + id + '/natjecanja').catch(()=>([]))
|
||
]);
|
||
const k = d.klub || {};
|
||
const stats = d.stats || {};
|
||
const clanoviArr = clanovi.rows || clanovi || [];
|
||
const docsArr = docs.rows || docs || [];
|
||
const natArr = Array.isArray(nat) ? nat : (nat.rows || []);
|
||
|
||
// Completeness calculation
|
||
const fields = ['oib','predsjednik','email','web','adresa','telefon','trener_glavni','iban','logo_url','opis_djelatnosti'];
|
||
const filled = fields.filter(f => k[f] && k[f] !== '–').length;
|
||
const compPct = Math.round(filled / fields.length * 100);
|
||
const compCol = compPct >= 80 ? '#22c55e' : compPct >= 50 ? '#f59e0b' : '#ef4444';
|
||
|
||
c.innerHTML = `
|
||
<div style="display:flex;align-items:center;gap:16px;margin-bottom:20px;flex-wrap:wrap">
|
||
<div style="width:56px;height:56px;border-radius:50%;background:linear-gradient(135deg,#1e3a5f,#0f172a);border:2px solid #f59e0b33;display:flex;align-items:center;justify-content:center;font-size:20px;flex-shrink:0">
|
||
${k.logo_url ? `<img src="${k.logo_url}" style="width:100%;height:100%;border-radius:50%;object-fit:cover" onerror="this.parentElement.innerHTML='⚽'">` : '⚽'}
|
||
</div>
|
||
<div style="flex:1;min-width:200px">
|
||
<div style="font-size:20px;font-weight:700;color:#e2e8f0">${k.naziv||'–'}</div>
|
||
<div style="color:#64748b;font-size:12px;margin-top:2px">
|
||
${k.sport ? `<span class="bdg" style="background:#0f172a;margin-right:6px">${k.sport}</span>` : ''}
|
||
${k.grad ? `<span style="margin-right:8px">📍 ${k.grad}</span>` : ''}
|
||
${k.savez_naziv ? `<span>🏛 ${k.savez_naziv}</span>` : ''}
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
||
${k.nositelj_kvalitete ? '<span style="background:#f59e0b22;color:#f59e0b;border:1px solid #f59e0b44;padding:4px 10px;border-radius:20px;font-size:11px;font-weight:700">⭐ Nositelj kvalitete</span>' : ''}
|
||
<div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:8px 14px;text-align:center">
|
||
<div style="font-size:18px;font-weight:700;color:${compCol}">${compPct}%</div>
|
||
<div style="font-size:9px;color:#475569;text-transform:uppercase">Kompletnost</div>
|
||
</div>
|
||
<button onclick="navigate('klubovi')" style="background:#0f172a;border:1px solid #1e293b;color:#64748b;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:11px">← Natrag</button>
|
||
<button onclick="enrichKlub(${k.id})" style="background:#0f172a;border:1px solid #f59e0b44;color:#f59e0b;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:11px">⚡ AI Enrich</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:16px">
|
||
${[
|
||
['Predsjednik', k.predsjednik],
|
||
['Tajnik', k.tajnik],
|
||
['Trener', k.trener_glavni],
|
||
['Email', k.email ? `<a href="mailto:${k.email}" style="color:#3b82f6">${k.email}</a>` : null],
|
||
['Web', k.web ? `<a href="${k.web}" target="_blank" style="color:#3b82f6">${k.web}</a>` : null],
|
||
['Telefon', k.telefon],
|
||
['OIB', k.oib],
|
||
['IBAN', k.iban],
|
||
['Adresa', k.adresa || k.sjediste],
|
||
['Reg. broj', k.reg_broj],
|
||
['Osnovan', k.datum_osnivanja_full || k.godina_osnutka],
|
||
['Razina natj.', k.razina],
|
||
['Status', k.udruga_status],
|
||
['HOO savez', k.savez_naziv],
|
||
].filter(([,v])=>v).map(([l,v])=>`
|
||
<div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:10px 12px">
|
||
<div style="font-size:9px;color:#475569;text-transform:uppercase;margin-bottom:3px">${l}</div>
|
||
<div style="color:#e2e8f0;font-size:12px;font-weight:500">${v}</div>
|
||
</div>`).join('')}
|
||
</div>
|
||
|
||
${k.opis_djelatnosti ? `
|
||
<div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:14px;margin-bottom:14px">
|
||
<div style="font-size:10px;color:#475569;text-transform:uppercase;margin-bottom:6px">Opis djelatnosti</div>
|
||
<div style="color:#94a3b8;font-size:12px;line-height:1.7">${k.opis_djelatnosti.slice(0,600)}${k.opis_djelatnosti.length>600?'…':''}</div>
|
||
</div>` : ''}
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:14px">
|
||
<div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px;text-align:center">
|
||
<div style="font-size:26px;font-weight:700;color:#3b82f6">${clanoviArr.length || k.broj_clanova || 0}</div>
|
||
<div style="font-size:10px;color:#475569">Članova</div>
|
||
</div>
|
||
<div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px;text-align:center">
|
||
<div style="font-size:26px;font-weight:700;color:#22c55e">${docsArr.length}</div>
|
||
<div style="font-size:10px;color:#475569">Dokumenata</div>
|
||
</div>
|
||
<div style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:12px;text-align:center">
|
||
<div style="font-size:26px;font-weight:700;color:#f59e0b">${natArr.length}</div>
|
||
<div style="font-size:10px;color:#475569">Natjecanja</div>
|
||
</div>
|
||
</div>
|
||
|
||
${clanoviArr.length ? `
|
||
<h3 style="color:#94a3b8;margin:16px 0 10px;font-size:13px;text-transform:uppercase;letter-spacing:1px">Članovi (${clanoviArr.length})</h3>
|
||
<div class="tbl-wrap"><table>
|
||
<thead><tr><th>Ime i prezime</th><th>Kategorija</th><th>Pozicija</th><th>Licenca vrijedi</th><th>Status</th></tr></thead>
|
||
<tbody>${clanoviArr.slice(0,50).map(c=>{
|
||
const licons = [];
|
||
if(c.reprezentativac) licons.push('<span title="Reprezentativac" style="color:#f59e0b">🏅</span>');
|
||
if(c.kategoriziran) licons.push('<span title="Kategoriziran HOO" style="color:#3b82f6">⭐</span>');
|
||
if(c.stipendiran) licons.push('<span title="Stipendiran" style="color:#22c55e">💰</span>');
|
||
return `<tr onclick="navigate('sportas_${c.id}')" style="cursor:pointer">
|
||
<td><b>${c.prezime||''} ${c.ime||''}</b></td>
|
||
<td class="dim" style="font-size:11px">${c.hoo_kategorija||c.kategorija||'–'}</td>
|
||
<td class="dim" style="font-size:11px">${c.pozicija||c.uloga||'–'}</td>
|
||
<td class="dim mono" style="font-size:10px">${c.licenca_vrijedi_do||'–'}</td>
|
||
<td style="white-space:nowrap">${licons.join('')} ${c.aktivan?'<span style="color:#22c55e;font-size:10px">●</span>':'<span style="color:#334155;font-size:10px">●</span>'}</td>
|
||
</tr>`;
|
||
}).join('')}
|
||
</tbody>
|
||
</table></div>` : '<div style="color:#334155;padding:16px;text-align:center;font-size:12px">Nema registriranih članova</div>'}
|
||
|
||
${docsArr.length ? `
|
||
<h3 style="color:#94a3b8;margin:16px 0 10px;font-size:13px;text-transform:uppercase;letter-spacing:1px">Dokumenti</h3>
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px">
|
||
${docsArr.slice(0,12).map(d=>`
|
||
<div onclick="viewDok(${d.id||d.did})" style="background:#0d1117;border:1px solid #1e293b;border-radius:8px;padding:10px 12px;cursor:pointer" onmouseover="this.style.borderColor='#f59e0b44'" onmouseout="this.style.borderColor='#1e293b'">
|
||
<div style="font-size:20px;margin-bottom:4px">${(d.tip||d.type||'').includes('pdf')?'📄':'📋'}</div>
|
||
<div style="font-size:11px;color:#e2e8f0;font-weight:500">${(d.naziv||d.title||'Dokument').slice(0,40)}</div>
|
||
<div style="font-size:10px;color:#475569;margin-top:3px">${d.tip||d.razina||'–'} · ${(d.created_at||'').slice(0,10)||'–'}</div>
|
||
</div>`).join('')}
|
||
</div>` : ''}
|
||
`;
|
||
} catch(e) { c.innerHTML = `<div class="ban crit">Greška: ${e.message}</div>`; }
|
||
}
|
||
|
||
async function pageClanovi() {
|
||
setTopbar('Organizacija', 'Članovi');
|
||
const c = document.getElementById('content');
|
||
const q = document.getElementById('q-clan')?.value||'';
|
||
const fkat = state.filters.kategorija||'';
|
||
const fspol = state.filters.spol||'';
|
||
const fr = state.filters.repr||'';
|
||
try {
|
||
const d = await api('/api/clanovi?'+(q?`q=${encodeURIComponent(q)}`:'')+(fkat?`&kategorija=${fkat}`:'')+(fspol?`&spol=${fspol}`:'')+(fr?`&reprezentativac=${fr}`:'')+getSort('clanovi'));
|
||
c.innerHTML = `
|
||
<div class="toolbar">
|
||
<input class="inp flex" id="q-clan" placeholder="🔍 Ime, OIB..." value="${q}" oninput="dbC()">
|
||
<select onchange="state.filters.kategorija=this.value;render()">
|
||
<option value="">Sve</option>
|
||
${['registrirani','neregistrirani','rekreativac','trener'].map(k=>`<option value="${k}" ${fkat===k?'selected':''}>${k}</option>`).join('')}
|
||
</select>
|
||
<select onchange="state.filters.spol=this.value;render()">
|
||
<option value="">Svi</option>
|
||
<option value="M" ${fspol==='M'?'selected':''}>M</option>
|
||
<option value="Ž" ${fspol==='Ž'?'selected':''}>Ž</option>
|
||
</select>
|
||
</div>
|
||
${!state.isAdmin?`<div class="ban warn"><b>🔒</b> Privatni podaci zamagljeni · klikni VIEWER za admin pristup</div>`:''}
|
||
<div class="tbl-wrap"><table>
|
||
<thead><tr>${tableHeader([{key:'prezime',label:'Prezime'},{key:'ime',label:'Ime'},{key:'oib',label:'OIB'},{key:'klub',label:'Klub'},{key:'kategorija',label:'Kat.'},{key:'datum_rodenja',label:'Rod.'},{key:'oib',label:'Liječ. do',sort:false},{key:'oib',label:'Dug',sort:false}], 'clanovi')}</tr></thead>
|
||
<tbody>${d.rows.length===0?'<tr><td colspan="8" class="empty">Nema članova</td></tr>':
|
||
d.rows.map(r=>`<tr>
|
||
<td><b>${r.prezime}</b></td>
|
||
<td>${r.ime}</td>
|
||
<td class="mono dim">${r.oib||'–'}</td>
|
||
<td>${r.klub_naziv||'–'}</td>
|
||
<td>${r.kategorija?`<span class="bdg info">${r.kategorija}</span>`:'–'}</td>
|
||
<td class="dim">${fmtDate(r.datum_rodenja)}</td>
|
||
<td>${r.lijecnicki_vrijedi_do?(new Date(r.lijecnicki_vrijedi_do)<new Date()?`<span class="bdg crit">${fmtDate(r.lijecnicki_vrijedi_do)}</span>`:`<span class="bdg ok">${fmtDate(r.lijecnicki_vrijedi_do)}</span>`):'–'}</td>
|
||
<td class="num mono">${r.dug_clanarine?`<span style="color:var(--crit)">${fmtEur(r.dug_clanarine)}</span>`:'–'}</td>
|
||
</tr>`).join('')}</tbody>
|
||
</table></div>
|
||
`;
|
||
} catch(e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
|
||
}
|
||
const dbC = debounce(pageClanovi, 300);
|
||
|
||
async function pageClanarine() {
|
||
setTopbar('Financije', 'Članarine');
|
||
const c = document.getElementById('content');
|
||
const fg = state.filters.godina||''; const fs = state.filters.status||'';
|
||
try {
|
||
const d = await api('/api/clanarine?'+(fg?`godina=${fg}`:'')+(fs?`&status=${fs}`:'')+getSort('clanarine'));
|
||
c.innerHTML = `
|
||
<div class="grid g4">
|
||
<div class="card"><div class="stat-l">Ukupno</div><div class="stat-v">${d.count}</div></div>
|
||
<div class="card acc"><div class="stat-l">Propisano</div><div class="stat-v sm">${fmtEur(d.summary.total_propisan)}</div></div>
|
||
<div class="card ok"><div class="stat-l">Plaćeno</div><div class="stat-v sm">${fmtEur(d.summary.total_placen)}</div></div>
|
||
<div class="card crit"><div class="stat-l">Dug</div><div class="stat-v sm">${fmtEur(d.summary.total_dug)}</div></div>
|
||
</div>
|
||
<div class="toolbar" style="margin-top:14px">
|
||
<select onchange="state.filters.godina=this.value;render()">
|
||
<option value="">Sve godine</option>
|
||
${[2026,2025,2024,2023,2022].map(y=>`<option value="${y}" ${fg==y?'selected':''}>${y}</option>`).join('')}
|
||
</select>
|
||
<select onchange="state.filters.status=this.value;render()">
|
||
<option value="">Svi statusi</option>
|
||
<option value="podmireno" ${fs==='podmireno'?'selected':''}>✓ Podmireno</option>
|
||
<option value="djelomicno" ${fs==='djelomicno'?'selected':''}>~ Djelomično</option>
|
||
<option value="nepodmireno" ${fs==='nepodmireno'?'selected':''}>✗ Nepodmireno</option>
|
||
</select>
|
||
</div>
|
||
<div class="tbl-wrap"><table>
|
||
<thead><tr>${tableHeader([{key:'godina',label:'God.'},{key:'klub',label:'Klub'},{key:'iznos',label:'Propisano'},{key:'iznos',label:'Plaćeno',sort:false},{key:'iznos',label:'Dug',sort:false},{key:'datum_uplate',label:'Uplata'},{key:'status',label:'Status'}], 'clanarine')}</tr></thead>
|
||
<tbody>${d.rows.length===0?'<tr><td colspan="7" class="empty">Bez zapisa</td></tr>':
|
||
d.rows.map(r=>`<tr>
|
||
<td><b>${r.godina}</b></td>
|
||
<td>${r.klub||'–'}</td>
|
||
<td class="num mono">${fmtEur(r.iznos_propisan)}</td>
|
||
<td class="num mono" style="color:var(--ok)">${fmtEur(r.iznos_placen)}</td>
|
||
<td class="num mono" style="color:${r.dug>0?'var(--crit)':'var(--text-3)'}">${fmtEur(r.dug)}</td>
|
||
<td class="dim">${fmtDate(r.datum_uplate)}</td>
|
||
<td><span class="bdg ${r.status==='podmireno'?'ok':r.status==='djelomicno'?'warn':'crit'}">${r.status}</span></td>
|
||
</tr>`).join('')}</tbody>
|
||
</table></div>
|
||
`;
|
||
} catch(e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
|
||
}
|
||
|
||
async function pageLijecnicki() {
|
||
setTopbar('Zdravlje', 'Liječnički pregledi');
|
||
const c = document.getElementById('content');
|
||
const fs = state.filters.status||'';
|
||
try {
|
||
const d = await api('/api/lijecnicki?'+(fs?`status=${encodeURIComponent(fs)}`:'')+getSort('lijecnicki'));
|
||
c.innerHTML = `
|
||
<div class="grid g4">
|
||
<div class="card"><div class="stat-l">Ukupno</div><div class="stat-v">${d.count}</div></div>
|
||
<div class="card crit"><div class="stat-l">Istekli</div><div class="stat-v">${d.summary?.istekli||0}</div></div>
|
||
<div class="card warn"><div class="stat-l">Uskoro</div><div class="stat-v">${d.summary?.uskoro||0}</div></div>
|
||
<div class="card acc"><div class="stat-l">ZZJZ udio</div><div class="stat-v sm">${fmtEur(d.summary?.total_zzjz)}</div></div>
|
||
</div>
|
||
<div class="toolbar" style="margin-top:14px">
|
||
<select onchange="state.filters.status=this.value;render()">
|
||
<option value="">Svi</option>
|
||
<option value="Validan" ${fs==='Validan'?'selected':''}>✓ Validni</option>
|
||
<option value="Ističe uskoro" ${fs==='Ističe uskoro'?'selected':''}>~ Uskoro</option>
|
||
<option value="Istekao" ${fs==='Istekao'?'selected':''}>✗ Istekli</option>
|
||
</select>
|
||
</div>
|
||
<div class="tbl-wrap"><table>
|
||
<thead><tr>${tableHeader([{key:'clan',label:'Sportaš'},{key:'klub',label:'Klub'},{key:'datum_pregleda',label:'Datum'},{key:'vrijedi_do',label:'Vrijedi do'},{key:'iznos',label:'Iznos'},{key:'iznos',label:'ZZJZ',sort:false},{key:'iznos',label:'Status',sort:false}], 'lijecnicki')}</tr></thead>
|
||
<tbody>${d.rows.length===0?'<tr><td colspan="7" class="empty">Bez zapisa</td></tr>':
|
||
d.rows.map(r=>`<tr>
|
||
<td><b>${r.clan}</b></td>
|
||
<td>${r.klub||'–'}</td>
|
||
<td class="dim">${fmtDate(r.datum_pregleda)}</td>
|
||
<td>${fmtDate(r.vrijedi_do)}</td>
|
||
<td class="num mono">${fmtEur(r.iznos)}</td>
|
||
<td class="num mono" style="color:var(--ok)">${fmtEur(r.iznos_zzjz)}</td>
|
||
<td><span class="bdg ${r.status_pregled==='Validan'?'ok':r.status_pregled==='Ističe uskoro'?'warn':'crit'}">${r.status_pregled}</span></td>
|
||
</tr>`).join('')}</tbody>
|
||
</table></div>
|
||
`;
|
||
} catch(e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
|
||
}
|
||
|
||
async function pagePotpore() {
|
||
setTopbar('Financije', 'Potpore nositeljima kvalitete');
|
||
const c = document.getElementById('content');
|
||
const fg = state.filters.godina||'';
|
||
try {
|
||
const d = await api('/api/potpore?'+(fg?`godina=${fg}`:'')+getSort('potpore'));
|
||
const total = d.rows.reduce((s,r)=>s+parseFloat(r.iznos||0),0);
|
||
c.innerHTML = `
|
||
<div class="grid g3">
|
||
<div class="card ok"><div class="stat-l">Ukupno</div><div class="stat-v sm">${fmtEur(total)}</div></div>
|
||
<div class="card"><div class="stat-l">Klubova</div><div class="stat-v">${new Set(d.rows.map(r=>r.naziv_kluba)).size}</div></div>
|
||
<div class="card"><div class="stat-l">Zapisa</div><div class="stat-v">${d.count}</div></div>
|
||
</div>
|
||
<div class="toolbar" style="margin-top:14px">
|
||
<select onchange="state.filters.godina=this.value;render()">
|
||
<option value="">Sve godine</option>
|
||
${[2025,2024,2023,2022,2021].map(y=>`<option value="${y}" ${fg==y?'selected':''}>${y}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="tbl-wrap"><table>
|
||
<thead><tr>${tableHeader([{key:'klub',label:'Klub'},{key:'godina',label:'God.'},{key:'iznos',label:'Iznos'}], 'potpore')}</tr></thead>
|
||
<tbody>${d.rows.map(r=>`<tr>
|
||
<td><b>${r.naziv_kluba}</b></td>
|
||
<td>${r.godina}</td>
|
||
<td class="num mono">${fmtEur(r.iznos)}</td>
|
||
</tr>`).join('')}</tbody>
|
||
</table></div>
|
||
`;
|
||
} catch(e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
|
||
}
|
||
|
||
async function pageProracun() {
|
||
setTopbar('Financije', 'Proračun PGŽ za sport');
|
||
const c = document.getElementById('content');
|
||
try {
|
||
const d = await api('/api/proracun');
|
||
const max = Math.max(...d.rows.map(r=>parseFloat(r.ukupno||0)),1);
|
||
c.innerHTML = `
|
||
<div class="card">
|
||
<div class="ct">📈 Trend 2016—2026 <span class="meta">${d.count} godina</span></div>
|
||
${d.rows.map(r=>`<div class="bar">
|
||
<div class="l"><b>${r.godina}.</b></div>
|
||
<div class="t"><div class="f ok" style="width:${(parseFloat(r.ukupno)/max*100).toFixed(1)}%"></div></div>
|
||
<div class="v">${fmtEur(r.ukupno)}</div>
|
||
</div>`).join('')}
|
||
</div>
|
||
<div class="sect">Detaljna tablica</div>
|
||
<div class="tbl-wrap"><table>
|
||
<thead><tr><th>God.</th><th style="text-align:right">PGŽ</th><th style="text-align:right">Reb.1</th><th style="text-align:right">Reb.2</th><th style="text-align:right">PGŽ uk.</th><th style="text-align:right">Min.</th><th style="text-align:right">UKUPNO</th></tr></thead>
|
||
<tbody>${d.rows.map(r=>`<tr>
|
||
<td><b>${r.godina}</b></td>
|
||
<td class="num mono">${fmtEur(r.proracun_pgz)}</td>
|
||
<td class="num mono">${fmtEur(r.rebalans1)}</td>
|
||
<td class="num mono">${fmtEur(r.rebalans2)}</td>
|
||
<td class="num mono">${fmtEur(r.ukupno_pgz)}</td>
|
||
<td class="num mono">${fmtEur(r.ministarstvo)}</td>
|
||
<td class="num mono" style="color:var(--ok);font-weight:700">${fmtEur(r.ukupno)}</td>
|
||
</tr>`).join('')}</tbody>
|
||
</table></div>
|
||
`;
|
||
} catch(e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
|
||
}
|
||
|
||
async function pageStatistika() {
|
||
setTopbar('Operativa', 'Statistika saveza');
|
||
const c = document.getElementById('content');
|
||
const fg = state.filters.godina || '2026';
|
||
const fr = state.filters.stat_razina !== undefined ? state.filters.stat_razina : 'zupanijski';
|
||
const fq = state.filters.stat_q || '';
|
||
try {
|
||
const d = await api('/api/statistika?godina='+fg+(fr?`&razina=${encodeURIComponent(fr)}`:'')+(fq?`&q=${encodeURIComponent(fq)}`:'')+getSort('statistika'));
|
||
c.innerHTML = `
|
||
<div class="toolbar">
|
||
<input class="inp flex" id="q-stat" placeholder="Pretraga saveza..." value="${fq}" oninput="dbStat()">
|
||
<select onchange="state.filters.godina=this.value;render()" title="Godina">
|
||
${[2026,2024,2023,2022,2021,2020].map(y=>`<option value="${y}" ${fg==y?'selected':''}>${y}.</option>`).join('')}
|
||
</select>
|
||
<select onchange="state.filters.stat_razina=this.value;render()" title="Razina saveza">
|
||
<option value="zupanijski" ${fr==='zupanijski'?'selected':''}>Županijski (PGŽ)</option>
|
||
<option value="gradski" ${fr==='gradski'?'selected':''}>Gradski</option>
|
||
<option value="opcinski" ${fr==='opcinski'?'selected':''}>Općinski</option>
|
||
<option value="strukovni" ${fr==='strukovni'?'selected':''}>Strukovni</option>
|
||
<option value="nacional" ${fr==='nacional'?'selected':''}>Nacionalni</option>
|
||
<option value="" ${fr===''?'selected':''}>Sve</option>
|
||
</select>
|
||
<span style="color:var(--text-3);font-size:11px;margin-left:auto">${d.count} saveza</span>
|
||
</div>
|
||
<div class="tbl-wrap"><table>
|
||
<thead><tr>${tableHeader([{key:'savez',label:'Savez'},{key:'klubova',label:'Klub.'},{key:'registriranih',label:'Reg.'},{key:'klubova',label:'Nereg.',sort:false},{key:'klubova',label:'Rekr.',sort:false},{key:'trenera',label:'Tren.'},{key:'reprezentativaca',label:'Repr.'}], 'statistika')}</tr></thead>
|
||
<tbody>${d.rows.map(r=>`<tr>
|
||
<td><b>${r.savez}</b></td>
|
||
<td class="num">${r.klubova_clanica}</td>
|
||
<td class="num">${r.registriranih}</td>
|
||
<td class="num dim">${r.neregistriranih}</td>
|
||
<td class="num dim">${r.rekreativaca}</td>
|
||
<td class="num">${r.trenera}</td>
|
||
<td class="num">${r.reprezentativaca}</td>
|
||
</tr>`).join('')}</tbody>
|
||
</table></div>
|
||
`;
|
||
} catch(e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
|
||
}
|
||
|
||
async function pageManifestacije() {
|
||
setTopbar('Operativa', 'Manifestacije');
|
||
const c = document.getElementById('content');
|
||
const fr = state.filters.razina||'';
|
||
try {
|
||
const d = await api('/api/manifestacije?'+(fr?`razina=${encodeURIComponent(fr)}`:'')+getSort('manifestacije'));
|
||
c.innerHTML = `
|
||
<div class="toolbar">
|
||
<select onchange="state.filters.razina=this.value;render()">
|
||
<option value="">Sve razine</option>
|
||
${['Klupska','Regionalna','Državna','Međunarodna'].map(r=>`<option value="${r}" ${fr===r?'selected':''}>${r}</option>`).join('')}
|
||
</select>
|
||
<span style="color:var(--text-3);font-size:11px;margin-left:auto">${d.count} manifestacija</span>
|
||
</div>
|
||
<div class="tbl-wrap"><table>
|
||
<thead><tr>${tableHeader([{key:'naziv',label:'Naziv'},{key:'mjesto',label:'Mjesto'},{key:'savez',label:'Savez',sort:false},{key:'razina',label:'Razina'},{key:'godina_od',label:'Od g.'},{key:'mjesto',label:'Učesnici',sort:false}], 'manifestacije')}</tr></thead>
|
||
<tbody>${d.rows.map(r=>`<tr>
|
||
<td><b>${r.naziv}</b></td>
|
||
<td>${r.mjesto||'–'}</td>
|
||
<td class="dim">${r.savez_naziv||'–'}</td>
|
||
<td><span class="bdg ${r.razina==='Međunarodna'?'gold':r.razina==='Državna'?'info':r.razina==='Regionalna'?'warn':'muted'}">${r.razina||'–'}</span></td>
|
||
<td class="num">${r.godina_od||'–'}</td>
|
||
<td class="dim">${r.broj_ucesnika||'–'}</td>
|
||
</tr>`).join('')}</tbody>
|
||
</table></div>
|
||
`;
|
||
} catch(e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
|
||
}
|
||
|
||
async function pageAlertovi() {
|
||
setTopbar('Pregled', 'Alertovi');
|
||
const c = document.getElementById('content');
|
||
try {
|
||
const d = await api('/api/alertovi?rijeseno=false');
|
||
c.innerHTML = `
|
||
<div class="toolbar">
|
||
<button class="btn warn" onclick="scanA()">↻ Skeniraj</button>
|
||
<span style="color:var(--text-3);font-size:11px;margin-left:auto">${d.count} aktivnih</span>
|
||
</div>
|
||
${d.count===0?'<div class="empty"><div class="empty-i">✓</div>Nema aktivnih alerta</div>':`
|
||
<div class="tbl-wrap"><table>
|
||
<thead><tr><th>Razina</th><th>Tip</th><th>Poruka</th><th>Datum</th><th style="text-align:right">Iznos</th><th>Akcija</th></tr></thead>
|
||
<tbody>${d.rows.map(r=>`<tr>
|
||
<td><span class="bdg ${r.razina==='CRITICAL'?'crit':r.razina==='WARNING'?'warn':'info'}">${r.razina}</span></td>
|
||
<td class="dim">${r.tip}</td>
|
||
<td>${r.poruka}</td>
|
||
<td class="dim">${fmtDate(r.datum)}</td>
|
||
<td class="num mono">${r.iznos?fmtEur(r.iznos):'–'}</td>
|
||
<td><button class="btn sec sm" onclick="event.stopPropagation();rijesi(${r.id})">Riješi</button></td>
|
||
</tr>`).join('')}</tbody>
|
||
</table></div>`}
|
||
`;
|
||
} catch(e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
|
||
}
|
||
async function scanA() { await fetch(API+'/api/alertovi/scan',{method:'POST',headers:{'Authorization':'Bearer '+state.token}}); pageAlertovi(); }
|
||
async function rijesi(id) { await fetch(API+'/api/alertovi/'+id+'/rijesi',{method:'PUT',headers:{'Authorization':'Bearer '+state.token}}); pageAlertovi(); }
|
||
|
||
async function pageZzjz() {
|
||
setTopbar('Zdravlje', 'ZZJZ PGŽ — Sufinanciranje');
|
||
const c = document.getElementById('content');
|
||
try {
|
||
const d = await api('/api/zzjz/dogovor');
|
||
const stv = d.stvarno_stanje||{};
|
||
c.innerHTML = `
|
||
<div class="ban info"><div><b>${d.info}</b><br><span style="opacity:0.85">${d.model}</span></div></div>
|
||
<div class="grid g3">
|
||
<div class="card"><div class="stat-l">Sportaša potencijalnih</div><div class="stat-v">${fmt(d.godisnji_potencijal?.sportasa_potencijalno)}</div></div>
|
||
<div class="card acc"><div class="stat-l">Procijenjeni godišnji</div><div class="stat-v sm">${fmtEur(d.godisnji_potencijal?.godisnji_trosak_eur)}</div></div>
|
||
<div class="card ok"><div class="stat-l">Pregleda u sustavu</div><div class="stat-v">${stv.pregleda||0}</div></div>
|
||
</div>
|
||
<div class="grid g2" style="margin-top:14px">
|
||
<div class="card">
|
||
<div class="ct">⚕ Stvarna podjela troškova</div>
|
||
<div class="donut-w">
|
||
${donut([parseFloat(stv.zzjz_isplata||0), parseFloat(stv.klub_isplata||0), parseFloat(stv.clan_isplata||0)],
|
||
['ZZJZ','Klub','Član'], ['#2DD4BF','#4A9EFF','#D4A852'],
|
||
fmt(parseFloat(stv.ukupan_trosak||0)), 'EUR')}
|
||
<div class="lg">
|
||
<div class="it"><div class="sw" style="background:#2DD4BF"></div><span class="lname">ZZJZ</span><span class="lval">${fmtEur(stv.zzjz_isplata)}</span></div>
|
||
<div class="it"><div class="sw" style="background:#4A9EFF"></div><span class="lname">Klub</span><span class="lval">${fmtEur(stv.klub_isplata)}</span></div>
|
||
<div class="it"><div class="sw" style="background:#D4A852"></div><span class="lname">Član</span><span class="lval">${fmtEur(stv.clan_isplata)}</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="ct">📋 Predviđeni tijek</div>
|
||
<ol style="padding-left:18px;line-height:1.9;font-size:12.5px;color:var(--text-2)">
|
||
<li><b>Klub registrira</b> sportaša u sustav</li>
|
||
<li><b>Sportaš odlazi</b> na liječnički u ZZJZ PGŽ</li>
|
||
<li><b>Liječnik unosi</b> nalaz, datum, vrijedi do</li>
|
||
<li><b>Sustav izračunava</b> udio: ZZJZ ⟷ klub ⟷ član</li>
|
||
<li><b>ZZJZ izdaje</b> račun klubu/PGŽ-u</li>
|
||
<li><b>Auto-alert</b> kada pregled ističe (60d/30d/0d)</li>
|
||
</ol>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} catch(e) { c.innerHTML = `<div class="ban crit">${e.message}</div>`; }
|
||
}
|
||
|
||
|
||
|
||
// === NAVIGACIJA: breadcrumbs, back, keyboard ===
|
||
window.navStack = window.navStack || [];
|
||
|
||
function navPush(page, params) {
|
||
// Push current state to history (max 20)
|
||
if (state.page) {
|
||
window.navStack.push({page: state.page, ...JSON.parse(JSON.stringify(state))});
|
||
if (window.navStack.length > 20) window.navStack.shift();
|
||
}
|
||
}
|
||
function navBack() {
|
||
if (window.navStack.length === 0) { goto('dashboard'); return; }
|
||
const prev = window.navStack.pop();
|
||
Object.assign(state, prev);
|
||
render();
|
||
}
|
||
// ESC = back
|
||
document.addEventListener('keydown', e => {
|
||
if (e.key === 'Escape' && window.navStack.length > 0 && !document.querySelector('.modal,dialog[open]')) {
|
||
e.preventDefault();
|
||
navBack();
|
||
}
|
||
});
|
||
|
||
function breadcrumbs(items) {
|
||
// items = [{label, onclick}]
|
||
let h = '<div class="breadcrumbs" style="display:flex;align-items:center;gap:8px;font-size:12px;color:var(--text2);margin-bottom:12px;flex-wrap:wrap">';
|
||
if (window.navStack.length > 0) {
|
||
h += '<button onclick="navBack()" title="Esc" style="background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:4px 10px;border-radius:6px;cursor:pointer;font-size:12px">← Nazad</button>';
|
||
h += '<span style="color:var(--text3)">·</span>';
|
||
}
|
||
items.forEach((it, i) => {
|
||
if (i > 0) h += '<span style="color:var(--text3)">›</span>';
|
||
if (it.onclick) {
|
||
h += `<a onclick="${it.onclick}" style="cursor:pointer;color:${i===items.length-1?'var(--text-bright)':'var(--accent)'}">${it.label}</a>`;
|
||
} else {
|
||
h += `<span style="color:${i===items.length-1?'var(--text-bright)':'var(--text2)'}">${it.label}</span>`;
|
||
}
|
||
});
|
||
h += '</div>';
|
||
return h;
|
||
}
|
||
|
||
// Prikaži vrijednost ili "NEDOSTAJE" jasno
|
||
function val(v, label) {
|
||
if (v === null || v === undefined || v === '') {
|
||
return `<span style="color:var(--text3);font-style:italic">— ${label||'nedostaje'} —</span>`;
|
||
}
|
||
return v;
|
||
}
|
||
function valWarn(v, type) {
|
||
// type='date'/'string' - vraća badge za nedostajuće s warning
|
||
if (v === null || v === undefined || v === '') {
|
||
return `<span class="risk-medium" style="font-size:9px;padding:2px 6px;border-radius:3px">PODATAK NEDOSTAJE</span>`;
|
||
}
|
||
return v;
|
||
}
|
||
|
||
|
||
const ULOGA_BADGE = {
|
||
'igrac': '<span style="background:rgba(59,130,246,0.15);color:#60a5fa;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">⚽ IGRAČ</span>',
|
||
'sportaš': '<span style="background:rgba(59,130,246,0.15);color:#60a5fa;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">🏃 SPORTAŠ</span>',
|
||
'sportas': '<span style="background:rgba(59,130,246,0.15);color:#60a5fa;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">🏃 SPORTAŠ</span>',
|
||
'predsjednik': '<span style="background:rgba(245,158,11,0.2);color:#f59e0b;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">👑 PREDSJEDNIK</span>',
|
||
'dopredsjednik': '<span style="background:rgba(245,158,11,0.15);color:#fbbf24;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">👑 DOPREDSJEDNIK</span>',
|
||
'tajnik': '<span style="background:rgba(168,85,247,0.15);color:#c084fc;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">📋 TAJNIK</span>',
|
||
'direktor': '<span style="background:rgba(168,85,247,0.2);color:#a855f7;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">📊 DIREKTOR</span>',
|
||
'trener': '<span style="background:rgba(34,197,94,0.15);color:#4ade80;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">📋 TRENER</span>',
|
||
'pomocni_trener': '<span style="background:rgba(34,197,94,0.12);color:#86efac;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">📋 POMOĆNI TRENER</span>',
|
||
'trener_vratara': '<span style="background:rgba(34,197,94,0.12);color:#86efac;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">🥅 TRENER VRATARA</span>',
|
||
'kondicioni_trener': '<span style="background:rgba(239,68,68,0.15);color:#f87171;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">💪 KONDICIONI</span>',
|
||
'fizioterapeut': '<span style="background:rgba(20,184,166,0.15);color:#5eead4;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">🩺 FIZIOTERAPEUT</span>',
|
||
'lijecnik': '<span style="background:rgba(244,63,94,0.15);color:#fb7185;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">⚕ LIJEČNIK</span>',
|
||
'sudac': '<span style="background:rgba(99,102,241,0.15);color:#818cf8;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">🟨 SUDAC</span>',
|
||
'član uprave': '<span style="background:rgba(245,158,11,0.1);color:#fcd34d;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">🪑 UPRAVA</span>',
|
||
'član nadzornog odbora': '<span style="background:rgba(245,158,11,0.1);color:#fcd34d;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">🔍 NO</span>',
|
||
'team_manager': '<span style="background:rgba(168,85,247,0.15);color:#c084fc;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">📊 MANAGER</span>',
|
||
'analiticar': '<span style="background:rgba(99,102,241,0.15);color:#818cf8;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">📈 ANALITIČAR</span>',
|
||
'video_analiticar': '<span style="background:rgba(99,102,241,0.15);color:#818cf8;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">🎥 VIDEO</span>',
|
||
'ostalo': '<span style="background:rgba(107,114,128,0.15);color:#9ca3af;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">— OSTALO</span>',
|
||
'': '<span style="background:rgba(107,114,128,0.1);color:#6b7280;padding:3px 8px;border-radius:4px;font-size:10px">—</span>',
|
||
'trener': '<span style="background:rgba(16,185,129,0.15);color:#10b981;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">📋 TRENER</span>',
|
||
'kondicioni_trener': '<span style="background:rgba(16,185,129,0.15);color:#10b981;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">💪 KONDICIONI TRENER</span>',
|
||
'direktor': '<span style="background:rgba(245,158,11,0.15);color:#f59e0b;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">📊 DIREKTOR</span>',
|
||
'predsjednik': '<span style="background:rgba(168,85,247,0.15);color:#a855f7;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">👑 PREDSJEDNIK</span>',
|
||
'tajnik': '<span style="background:rgba(168,85,247,0.15);color:#a855f7;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">📝 TAJNIK</span>',
|
||
'fizioterapeut': '<span style="background:rgba(236,72,153,0.15);color:#ec4899;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">⚕️ FIZIO</span>',
|
||
'lijecnik': '<span style="background:rgba(236,72,153,0.15);color:#ec4899;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">⚕️ LIJEČNIK</span>',
|
||
'sudac': '<span style="background:rgba(107,114,128,0.15);color:#9ca3af;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">🦮 SUDAC</span>',
|
||
'ostalo': '<span style="background:rgba(107,114,128,0.15);color:#9ca3af;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600">▪ OSTALO</span>',
|
||
};
|
||
function ulogaBadge(u) { return ULOGA_BADGE[u] || (u ? '<span style="color:var(--text3);font-size:10px">'+u+'</span>' : ''); }
|
||
|
||
// Sport ikone (emoji) za pregled svih sportova
|
||
const SPORT_ICONS = {
|
||
'nogomet': '⚽', 'rukomet': '🤾', 'košarka': '🏀', 'kosarka': '🏀',
|
||
'vaterpolo': '🤽', 'odbojka': '🏐', 'tenis': '🎾', 'stolni tenis': '🏓',
|
||
'atletika': '🏃', 'plivanje': '🏊', 'biciklizam': '🚴', 'boks': '🥊',
|
||
'karate': '🥋', 'judo': '🥋', 'taekwondo': '🥋', 'kickboxing': '🥊',
|
||
'jedriličarstvo': '⛵', 'jedrilicarstvo': '⛵', 'skijanje': '⛷️',
|
||
'ribolov': '🎣', 'šah': '♟️', 'sah': '♟️', 'streljaštvo': '🎯',
|
||
'streličarstvo': '🏹', 'gimnastika': '🤸', 'ples': '💃', 'kuglanje': '🎳',
|
||
'pikado': '🎯', 'planinarstvo': '🏔️', 'konjički sport': '🐎',
|
||
'veslanje': '🚣', 'mačevanje': '🤺', 'hrvanje': '🤼', 'penjanje': '🧗',
|
||
'multisport': '🏆', 'rekreacija': '🎯', 'motosport': '🏎️',
|
||
'baseball': '⚾', 'softball': '⚾', 'golf': '⛳', 'hokej': '🏒',
|
||
'parasport': '♿', 'parasportski': '♿', 'paraolimpijski': '♿',
|
||
'lov': '🦌', 'kajakaštvo': '🛶', 'curling': '🥌', 'eSport': '🎮',
|
||
'borilački sport': '🥋', 'penjanje': '🧗', 'sport gluhih': '🤟',
|
||
'sport slijepih': '👁️🗨️', 'olimpijski': '🏅', 'kineziologija': '📚',
|
||
'medicina': '⚕️', 'školski sport': '🎒', 'općenito': '🏟️',
|
||
};
|
||
function sportIcon(s) { return SPORT_ICONS[(s||'').toLowerCase()] || '🏃'; }
|
||
|
||
|
||
|
||
// === GUI UPGRADE - sprint 5 ===
|
||
function showSportasiModal(sport, filter, title) {
|
||
const params = new URLSearchParams();
|
||
if (sport) params.set('sport', sport);
|
||
if (filter === 'reprezentativci') params.set('reprezentativac', 'true');
|
||
if (filter === 'kategorizirani') params.set('kategorija_min', '1');
|
||
if (filter === 'klubovi') {
|
||
// Klubovi modal handled separately
|
||
return showKlubModal(sport, title);
|
||
}
|
||
if (filter === 'savezi') {
|
||
return showSavezModal(sport, title);
|
||
}
|
||
|
||
fetch('/sport/api/v2/clanovi?' + params.toString() + '&limit=200')
|
||
.then(r => r.json()).then(d => {
|
||
const items = d.data || d || [];
|
||
let html = '<div class="ri-modal" id="riModal" onclick="if(event.target===this)closeRiModal()">';
|
||
html += '<div class="ri-modal-box" style="max-width:1100px">';
|
||
html += '<div class="ri-modal-h"><h3 style="margin:0">' + title + ' <span class="muted" style="font-size:12px">(' + items.length + ')</span></h3>';
|
||
html += '<button class="ri-icon-btn" onclick="closeRiModal()" title="Zatvori">' + iconX() + '</button></div>';
|
||
html += '<div class="ri-modal-body">';
|
||
html += '<table class="ri-tbl ri-sortable" data-table="modal-sportasi"><thead><tr>';
|
||
html += '<th>Foto</th>';
|
||
html += '<th class="ri-sort" data-key="ime">Ime i prezime</th>';
|
||
html += '<th class="ri-sort" data-key="klub_naziv">Klub</th>';
|
||
html += '<th class="ri-sort" data-key="sport">Sport</th>';
|
||
html += '<th class="ri-sort" data-key="uloga">Uloga</th>';
|
||
html += '<th class="ri-sort num" data-key="kategorija_hoo">HOO</th>';
|
||
html += '<th class="ri-sort" data-key="reprezentativac">Repr.</th>';
|
||
html += '<th>Akcije</th>';
|
||
html += '</tr></thead><tbody>';
|
||
items.forEach(s => {
|
||
const ime = (s.ime||'') + ' ' + (s.prezime||'');
|
||
html += '<tr style="cursor:pointer" onclick="closeRiModal();gotoSportas(' + s.id + ')">';
|
||
html += '<td>' + (s.slika_url ? '<img src="' + imgProxy(s.slika_url) + '" style="width:30px;height:30px;border-radius:50%;object-fit:cover"/>' : '<span style="color:var(--text3)">' + iconUser() + '</span>') + '</td>';
|
||
html += '<td><b>' + ime + '</b></td>';
|
||
html += '<td class="muted">' + (s.klub_naziv || '-') + '</td>';
|
||
html += '<td>' + (s.sport || '-') + '</td>';
|
||
html += '<td>' + (s.uloga || '-') + '</td>';
|
||
html += '<td class="num">' + (s.kategorija_hoo ? '<span class="ri-badge ri-badge-gold">' + 'I'.repeat(s.kategorija_hoo) + '</span>' : '-') + '</td>';
|
||
html += '<td>' + (s.reprezentativac ? '<span class="ri-badge ri-badge-blue">REPR</span>' : '-') + '</td>';
|
||
html += '<td onclick="event.stopPropagation()">';
|
||
html += '<button class="ri-icon-btn-sm" onclick="closeRiModal();gotoSportas(' + s.id + ')" title="View">' + iconEye() + '</button>';
|
||
html += '</td>';
|
||
html += '</tr>';
|
||
});
|
||
html += '</tbody></table>';
|
||
html += '</div></div></div>';
|
||
|
||
const div = document.createElement('div');
|
||
div.innerHTML = html;
|
||
document.body.appendChild(div.firstChild);
|
||
|
||
// Attach sort handlers
|
||
attachTableSort(document.querySelector('#riModal .ri-sortable'));
|
||
}).catch(e => alert('Greška: ' + e));
|
||
}
|
||
|
||
function showKlubModal(sport, title) {
|
||
fetch('/sport/api/v2/klubovi/sa-clanstvom?sport=' + encodeURIComponent(sport || '') + '&limit=300')
|
||
.then(r => r.json()).then(d => {
|
||
const items = (d.data || d || []).filter(k => !sport || (k.sport||'').toLowerCase() === sport.toLowerCase());
|
||
let html = '<div class="ri-modal" id="riModal" onclick="if(event.target===this)closeRiModal()">';
|
||
html += '<div class="ri-modal-box" style="max-width:1100px">';
|
||
html += '<div class="ri-modal-h"><h3 style="margin:0">' + title + ' <span class="muted" style="font-size:12px">(' + items.length + ')</span></h3>';
|
||
html += '<button class="ri-icon-btn" onclick="closeRiModal()">' + iconX() + '</button></div>';
|
||
html += '<div class="ri-modal-body">';
|
||
html += '<table class="ri-tbl ri-sortable" data-table="modal-klubovi"><thead><tr>';
|
||
html += '<th class="ri-sort" data-key="naziv">Klub</th>';
|
||
html += '<th class="ri-sort" data-key="sport">Sport</th>';
|
||
html += '<th class="ri-sort" data-key="razina">Razina</th>';
|
||
html += '<th class="ri-sort" data-key="grad">Grad</th>';
|
||
html += '<th class="ri-sort num" data-key="broj_clanova">Članova</th>';
|
||
html += '<th>Akcije</th>';
|
||
html += '</tr></thead><tbody>';
|
||
items.forEach(k => {
|
||
html += '<tr style="cursor:pointer" onclick="closeRiModal();gotoKlubRoster(' + k.id + ')">';
|
||
html += '<td><b>' + (k.naziv||'') + '</b></td>';
|
||
html += '<td>' + (k.sport || '-') + '</td>';
|
||
html += '<td>' + (k.razina || '-') + '</td>';
|
||
html += '<td class="muted">' + (k.grad || '-') + '</td>';
|
||
html += '<td class="num">' + (k.broj_clanova || 0) + '</td>';
|
||
html += '<td onclick="event.stopPropagation()">';
|
||
html += '<button class="ri-icon-btn-sm" onclick="closeRiModal();gotoKlubRoster(' + k.id + ')" title="View">' + iconEye() + '</button>';
|
||
html += '</td>';
|
||
html += '</tr>';
|
||
});
|
||
html += '</tbody></table>';
|
||
html += '</div></div></div>';
|
||
const div = document.createElement('div');
|
||
div.innerHTML = html;
|
||
document.body.appendChild(div.firstChild);
|
||
attachTableSort(document.querySelector('#riModal .ri-sortable'));
|
||
});
|
||
}
|
||
|
||
function showSavezModal(sport, title) {
|
||
fetch('/sport/api/v2/savezi?sport=' + encodeURIComponent(sport || ''))
|
||
.then(r => r.json()).then(d => {
|
||
const items = d.data || d || [];
|
||
let html = '<div class="ri-modal" id="riModal" onclick="if(event.target===this)closeRiModal()">';
|
||
html += '<div class="ri-modal-box" style="max-width:900px">';
|
||
html += '<div class="ri-modal-h"><h3>' + title + ' <span class="muted">(' + items.length + ')</span></h3>';
|
||
html += '<button class="ri-icon-btn" onclick="closeRiModal()">' + iconX() + '</button></div>';
|
||
html += '<div class="ri-modal-body"><table class="ri-tbl ri-sortable"><thead><tr>';
|
||
html += '<th class="ri-sort" data-key="naziv">Savez</th>';
|
||
html += '<th class="ri-sort" data-key="razina">Razina</th>';
|
||
html += '<th class="ri-sort" data-key="grad">Grad</th>';
|
||
html += '<th class="ri-sort" data-key="predsjednik">Predsjednik</th>';
|
||
html += '</tr></thead><tbody>';
|
||
items.forEach(s => {
|
||
html += '<tr><td><b>' + (s.naziv||'') + '</b></td><td>' + (s.razina||'-') + '</td><td class="muted">' + (s.grad||'-') + '</td><td>' + (s.predsjednik||'-') + '</td></tr>';
|
||
});
|
||
html += '</tbody></table></div></div></div>';
|
||
const div = document.createElement('div');
|
||
div.innerHTML = html;
|
||
document.body.appendChild(div.firstChild);
|
||
attachTableSort(document.querySelector('#riModal .ri-sortable'));
|
||
});
|
||
}
|
||
|
||
function closeRiModal() {
|
||
const m = document.getElementById('riModal');
|
||
if (m) m.remove();
|
||
}
|
||
|
||
// === Table sort ===
|
||
function attachTableSort(tableEl) {
|
||
if (!tableEl) return;
|
||
const ths = tableEl.querySelectorAll('th.ri-sort');
|
||
ths.forEach(th => {
|
||
th.style.cursor = 'pointer';
|
||
th.style.userSelect = 'none';
|
||
th.addEventListener('click', () => {
|
||
const key = th.dataset.key;
|
||
const tbody = tableEl.querySelector('tbody');
|
||
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||
const isNum = th.classList.contains('num');
|
||
const cur = th.dataset.dir || '';
|
||
const dir = cur === 'asc' ? 'desc' : 'asc';
|
||
// clear other ths
|
||
tableEl.querySelectorAll('th.ri-sort').forEach(t2 => {
|
||
t2.dataset.dir = '';
|
||
const arr = t2.querySelector('.ri-sort-arrow');
|
||
if (arr) arr.remove();
|
||
});
|
||
th.dataset.dir = dir;
|
||
const arrow = document.createElement('span');
|
||
arrow.className = 'ri-sort-arrow';
|
||
arrow.textContent = dir === 'asc' ? ' ↑' : ' ↓';
|
||
arrow.style.color = 'var(--accent)';
|
||
arrow.style.fontSize = '10px';
|
||
th.appendChild(arrow);
|
||
|
||
// Sort rows by cell at index of th
|
||
const idx = Array.from(th.parentElement.children).indexOf(th);
|
||
rows.sort((a, b) => {
|
||
const av = a.children[idx]?.innerText.trim() || '';
|
||
const bv = b.children[idx]?.innerText.trim() || '';
|
||
let cmp;
|
||
if (isNum) {
|
||
cmp = (parseFloat(av.replace(/[^\d.\-]/g,'')) || 0) - (parseFloat(bv.replace(/[^\d.\-]/g,'')) || 0);
|
||
} else {
|
||
cmp = av.localeCompare(bv, 'hr', {numeric: true});
|
||
}
|
||
return dir === 'asc' ? cmp : -cmp;
|
||
});
|
||
rows.forEach(r => tbody.appendChild(r));
|
||
});
|
||
});
|
||
}
|
||
|
||
// Auto-attach sort on every page render
|
||
function autoAttachSort() {
|
||
document.querySelectorAll('table.ri-sortable').forEach(t => {
|
||
if (!t.dataset.sortAttached) {
|
||
attachTableSort(t);
|
||
t.dataset.sortAttached = '1';
|
||
}
|
||
});
|
||
}
|
||
|
||
// === Unified Lucide icons (SVG) ===
|
||
function iconEye() { return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>'; }
|
||
function iconEdit() { return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>'; }
|
||
function iconShare() { return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>'; }
|
||
function iconDownload() { return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>'; }
|
||
function iconX() { return '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>'; }
|
||
function iconUser() { return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>'; }
|
||
function iconFile() { return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>'; }
|
||
function iconExternal() { return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>'; }
|
||
function iconMic() { return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>'; }
|
||
|
||
// === GUI UPGRADE end ===
|
||
|
||
|
||
const routes = { graf:pageGraf, dms:pageDMS, dataQuality:pageDataQuality, search:pageSearch, dashboard:pageDashboard, analytics:pageAnalytics, alertovi:pageAlertovi, savezi:pageSavezi, klubovi:pageKlubovi, clanovi:pageClanovi, clanarine:pageClanarine, potpore:pagePotpore, proracun:pageProracun, lijecnicki:pageLijecnicki, zzjz:pageZzjz, manifestacije:pageManifestacije, statistika:pageStatistika , ask:pageAsk, invoices:pageInvoices, expenses:pageExpenses, forms:pageForms, users:pageUsers, pravnik:pagePravnik, natjecanja:pageNatjecanja, admin:pageAdmin , sportStats:pageSportStats, baza:pageBaza, dokumenti:pageDokumenti, kategorije:pageKategorije, funkcionari:pageFunkcionari, sportasi:pageSportasi, sportas:pageSportas, klubRoster:pageKlubRoster, sport:pageSport, audit:pageAudit, matrix:pageMatrix, coverage:pageCoverage, natjecanja:pageNatjecanja, natjecanjaTablica:pageNatjecanjaTablica, godisnjaci:pageGodisnjaci, dokumentDetail:pageDokumentDetail };
|
||
|
||
|
||
// ===== V6.2 VOICE INPUT (hr-HR) =====
|
||
window._v6Recognition = null;
|
||
window._v6CurrentInput = null;
|
||
function v6VoiceInit() {
|
||
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||
if (!SR) return null;
|
||
const r = new SR();
|
||
r.lang = 'hr-HR';
|
||
r.continuous = false;
|
||
r.interimResults = true;
|
||
r.maxAlternatives = 1;
|
||
return r;
|
||
}
|
||
function v6VoiceStart(inputId, btnEl) {
|
||
const inp = document.getElementById(inputId);
|
||
if (!inp) return;
|
||
if (!window._v6Recognition) window._v6Recognition = v6VoiceInit();
|
||
const rec = window._v6Recognition;
|
||
if (!rec) {
|
||
alert('Voice input nije podržan u ovom pregledniku. Koristi Chrome ili Edge.');
|
||
return;
|
||
}
|
||
// If already recording, stop
|
||
if (btnEl && btnEl.classList.contains('recording')) {
|
||
try { rec.stop(); } catch(e){}
|
||
btnEl.classList.remove('recording');
|
||
btnEl.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>';
|
||
return;
|
||
}
|
||
if (btnEl) {
|
||
btnEl.classList.add('recording');
|
||
btnEl.innerHTML = '■';
|
||
}
|
||
let finalTranscript = '';
|
||
rec.onresult = function(ev) {
|
||
let interim = '';
|
||
for (let i = ev.resultIndex; i < ev.results.length; i++) {
|
||
if (ev.results[i].isFinal) finalTranscript += ev.results[i][0].transcript;
|
||
else interim += ev.results[i][0].transcript;
|
||
}
|
||
inp.value = finalTranscript + interim;
|
||
};
|
||
rec.onerror = function(ev) {
|
||
console.warn('voice err', ev.error);
|
||
if (btnEl) { btnEl.classList.remove('recording'); btnEl.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>'; }
|
||
};
|
||
rec.onend = function() {
|
||
if (btnEl) { btnEl.classList.remove('recording'); btnEl.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>'; }
|
||
// If we got final transcript and inp is part of search/ask form, auto-submit
|
||
if (finalTranscript) {
|
||
inp.value = finalTranscript.trim();
|
||
// Auto-submit based on input id
|
||
if (inputId === 'askQ' && typeof askGo === 'function') askGo();
|
||
else if (inputId === 'lawQ' && typeof lawGo === 'function') lawGo();
|
||
else if (inputId === 'aiSearchInline' && finalTranscript.trim()) {
|
||
state.searchQ = finalTranscript.trim();
|
||
render();
|
||
}
|
||
else if (inputId === 'searchInput' && finalTranscript.trim()) {
|
||
state.searchQ = finalTranscript.trim();
|
||
render();
|
||
}
|
||
}
|
||
};
|
||
try { rec.start(); }
|
||
catch(e) {
|
||
console.warn('voice start err', e);
|
||
if (btnEl) { btnEl.classList.remove('recording'); btnEl.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>'; }
|
||
}
|
||
}
|
||
|
||
// ===== V6.2 CHATBOT for AI Asistent =====
|
||
window.chatHistory = []; // array of {role:'user'|'bot', content, sources?, llm?, hits?}
|
||
|
||
function chatRender() {
|
||
const t = document.getElementById('chatThread');
|
||
if (!t) return;
|
||
t.innerHTML = chatHistory.map(function(m, i){
|
||
if (m.role === 'user') {
|
||
return '<div class="v6-chat-msg user">' + (m.content||'').replace(/</g,'<') + '</div>';
|
||
} else if (m.role === 'typing') {
|
||
return '<div class="v6-chat-typing"><span></span><span></span><span></span></div>';
|
||
} else {
|
||
let metaHtml = '';
|
||
if (m.llm) metaHtml = '<div class="v6-msg-meta">🤖 ' + m.llm + (m.hits ? ' · '+m.hits+' izvora' : '') + '</div>';
|
||
|
||
// CLICKABLE RESULTS MODE (RAG)
|
||
if (m.isClickable && m.results) {
|
||
let bodyHtml = '<div style="margin-bottom:8px">Pronašao sam ' + m.totalCount + ' rezultata. Klikni za otvaranje:</div>';
|
||
bodyHtml += '<div style="display:flex;flex-direction:column;gap:6px">';
|
||
m.results.forEach(function(h, i) {
|
||
const title = h.title || (h.payload && h.payload.title) || '?';
|
||
const snippet = (h.snippet || '').slice(0, 180);
|
||
const score = (h.score*100).toFixed(0);
|
||
const tip = (h.payload && (h.payload.tip || h.payload.type)) || h.type || '';
|
||
const p = h.payload || {};
|
||
|
||
// Build click handler based on tip - use existing goto* functions
|
||
let onClick = '';
|
||
let icon = '👤';
|
||
if (tip === 'clan' && p.clan_id) {
|
||
onClick = `gotoSportas(${p.clan_id})`;
|
||
icon = '🏃';
|
||
} else if (tip === 'klub' && p.klub_id) {
|
||
onClick = `gotoKlubRoster(${p.klub_id})`;
|
||
icon = '🏛️';
|
||
} else if (tip === 'savez' && (p.savez_id || p.id)) {
|
||
onClick = `goto('savezi')`;
|
||
icon = '🏆';
|
||
} else if (tip === 'dokument' && (p.dokument_id || p.id)) {
|
||
onClick = `showDokViewerModal(${p.dokument_id || p.id})`;
|
||
icon = '📄';
|
||
} else if (tip === 'manifestacija' && p.manifestacija_id) {
|
||
onClick = `state.manifestacija_id=${p.manifestacija_id};goto('manifestacije')`;
|
||
icon = '🎯';
|
||
}
|
||
|
||
if (onClick) {
|
||
bodyHtml += '<div onclick="' + onClick.replace(/"/g,'"') + '" style="cursor:pointer;padding:10px 12px;background:var(--bg-2);border-radius:6px;border-left:3px solid var(--accent);transition:background 0.2s" onmouseover="this.style.background='var(--bg-3)'" onmouseout="this.style.background='var(--bg-2)'">';
|
||
bodyHtml += '<div style="display:flex;align-items:center;gap:8px"><span>' + icon + '</span><b>' + title.replace(/</g,'<') + '</b>';
|
||
bodyHtml += '<span style="margin-left:auto;color:var(--text3);font-size:11px">' + score + '%</span></div>';
|
||
if (snippet) bodyHtml += '<div class="muted" style="font-size:12px;margin-top:4px;line-height:1.4">' + snippet.replace(/</g,'<') + '</div>';
|
||
bodyHtml += '</div>';
|
||
} else {
|
||
bodyHtml += '<div style="padding:10px 12px;background:var(--bg-2);border-radius:6px;opacity:0.6">';
|
||
bodyHtml += '<b>' + title.replace(/</g,'<') + '</b> <span style="color:var(--text3);font-size:11px">' + score + '%</span>';
|
||
if (snippet) bodyHtml += '<div class="muted" style="font-size:12px;margin-top:4px">' + snippet.replace(/</g,'<') + '</div>';
|
||
bodyHtml += '</div>';
|
||
}
|
||
});
|
||
bodyHtml += '</div>';
|
||
// AI enrich button if low scores or fewer results
|
||
if (m.query) {
|
||
// Use data attribute for safe escape, click handled below
|
||
const qEsc = (m.query || '').replace(/"/g, '"').replace(/</g, '<');
|
||
bodyHtml += `<div style="margin-top:12px;text-align:right"><button class="btn primary chat-enrich-btn" data-q="${qEsc}" style="font-size:12px">🔍 AI obogati pretragu (Internet)</button></div>`;
|
||
}
|
||
return '<div class="v6-chat-msg bot">' + metaHtml + bodyHtml + '</div>';
|
||
}
|
||
|
||
// Fallback: plain text + sources (lawyer mode etc.)
|
||
let body = (m.content||'').replace(/</g,'<');
|
||
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,'"') + '" target="_blank">📄 ' + (title.length>40?title.slice(0,40)+'…':title) + '</a>';
|
||
return '<span class="v6-src-link" style="background:#2a3a52">📌 '+title+'</span>';
|
||
}).join('') + '</div>';
|
||
}
|
||
// Show enrich button if no results
|
||
let enrichHtml = '';
|
||
if (m.enrichQuery) {
|
||
const qEsc2 = (m.enrichQuery || '').replace(/"/g, '"').replace(/</g, '<');
|
||
enrichHtml = `<div style="margin-top:12px;text-align:right"><button class="btn primary chat-enrich-btn" data-q="${qEsc2}" style="font-size:12px">🔍 AI obogati (Internet)</button></div>`;
|
||
}
|
||
return '<div class="v6-chat-msg bot">' + metaHtml + body + srcHtml + enrichHtml + '</div>';
|
||
}
|
||
}).join('');
|
||
t.scrollTop = t.scrollHeight;
|
||
}
|
||
|
||
async function chatSend(mode) {
|
||
// mode: 'rag' (askGo) or 'lawyer' (lawGo)
|
||
const inp = document.getElementById(mode === 'lawyer' ? 'lawQ' : 'askQ');
|
||
const q = inp.value.trim();
|
||
if (!q) return;
|
||
chatHistory.push({role:'user', content:q});
|
||
chatHistory.push({role:'typing'});
|
||
chatRender();
|
||
inp.value = '';
|
||
|
||
try {
|
||
let endpoint, payload;
|
||
if (mode === 'lawyer') {
|
||
endpoint = '/sport/api/v2/sport/lawyer';
|
||
// include conversation context (last 4 turns)
|
||
const ctx = chatHistory.slice(-8).filter(function(m){ return m.role !== 'typing'; })
|
||
.map(function(m){ return (m.role==='user'?'Q: ':'A: ') + (m.content||'').slice(0,300); }).join('\n');
|
||
payload = {query: q, context: ctx};
|
||
} else {
|
||
endpoint = '/sport/api/v2/sport/ask';
|
||
payload = {query: q, limit: 8};
|
||
}
|
||
const r = await fetch(endpoint, {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload)});
|
||
const d = await r.json();
|
||
|
||
// Remove typing indicator
|
||
chatHistory = chatHistory.filter(function(m){ return m.role !== 'typing'; });
|
||
|
||
if (mode === 'lawyer') {
|
||
chatHistory.push({
|
||
role: 'bot',
|
||
content: d.answer || d.detail || 'Nema odgovora.',
|
||
sources: d.sources,
|
||
llm: d.llm,
|
||
hits: d.hits_count
|
||
});
|
||
} else {
|
||
// RAG mode — clickable HTML rendering
|
||
if (!d.results || !d.results.length) {
|
||
chatHistory.push({role:'bot', content:'Nisam pronašao ništa relevantno za "'+q+'".', enrichQuery: q});
|
||
} else {
|
||
const top5 = d.results.slice(0, 5);
|
||
chatHistory.push({role:'bot', isClickable: true, query: q, results: top5, totalCount: d.results.length, llm:'rag', hits: d.results.length});
|
||
}
|
||
}
|
||
} catch(e) {
|
||
chatHistory = chatHistory.filter(function(m){ return m.role !== 'typing'; });
|
||
chatHistory.push({role:'bot', content: '⚠️ Greška: ' + e.message});
|
||
}
|
||
chatRender();
|
||
}
|
||
|
||
function chatReset() {
|
||
chatHistory = [];
|
||
chatRender();
|
||
const t = document.getElementById('chatThread');
|
||
if (t) t.innerHTML = '<div style="text-align:center;color:#788798;padding:40px;font-size:13px">💬 Postavi pitanje. Mogu odgovoriti glasovno (<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>) ili tekstom.</div>';
|
||
}
|
||
|
||
|
||
function render() {
|
||
const fn = routes[state.page] || pageDashboard;
|
||
fn().then(() => {
|
||
// Auto-attach sort to all .ri-sortable tables after render
|
||
setTimeout(() => { try { autoAttachSort(); } catch(e){} }, 50);
|
||
setTimeout(() => { try { autoAttachSort(); } catch(e){} }, 300);
|
||
}).catch(e => document.getElementById('content').innerHTML = `<div class="ban crit"><b>Greška:</b> ${e.message}</div>`);
|
||
}
|
||
|
||
// Global MutationObserver - re-attach sort on any DOM change in #content
|
||
window.addEventListener('DOMContentLoaded', () => {
|
||
const target = document.getElementById('content');
|
||
if (target && window.MutationObserver) {
|
||
const obs = new MutationObserver(() => { try { autoAttachSort(); } catch(e){} });
|
||
obs.observe(target, { childList:true, subtree:true });
|
||
}
|
||
});
|
||
|
||
document.getElementById('token-input').addEventListener('keypress', e => { if (e.key==='Enter') doLogin(); });
|
||
|
||
|
||
// ============ V2 ERP & PRAVO PAGES ============
|
||
async function v2Fetch(path, opts={}) {
|
||
const tok = localStorage.getItem('rinet_v2_token');
|
||
opts.headers = Object.assign({'Content-Type':'application/json'}, opts.headers||{}, tok?{Authorization:'Bearer '+tok}:{});
|
||
const r = await fetch('/sport/api/v2'+path, opts);
|
||
if (!r.ok) throw new Error(`${r.status}: ${await r.text()}`);
|
||
return r.json();
|
||
}
|
||
|
||
async function pageAsk() {
|
||
document.getElementById('content').innerHTML = `
|
||
<div class="page-h"><h2>AI Asistent</h2><p class="muted">Razgovaraj sa AI asistentom o klubovima, savezima, pravilnicima, financiranju. Tipkaj ili koristi glasovni unos <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg> (hr-HR).</p></div>
|
||
<div class="card">
|
||
<div id="chatThread" class="v6-chat-thread"></div>
|
||
<div class="v6-input-row">
|
||
<input id="askQ" class="inp" style="flex:1" placeholder="Postavi pitanje... (Enter za poslati)" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();chatSend('rag')}" />
|
||
<button class="v6-mic-btn" id="askMicBtn" onclick="v6VoiceStart('askQ', this)" title="Glasovni unos (hr-HR)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></button>
|
||
<button class="btn primary" onclick="chatSend('rag')">📤</button>
|
||
<button class="btn" onclick="chatReset()" title="Novi razgovor">Reset</button>
|
||
</div>
|
||
<div style="font-size:12px;color:var(--muted);margin-top:8px">Primjeri:
|
||
<a href="#" onclick="document.getElementById('askQ').value='Pravilnik o liječničkim pregledima sportaša';chatSend('rag');return false">Liječnički</a> ·
|
||
<a href="#" onclick="document.getElementById('askQ').value='Kako se financiraju javne potrebe u sportu PGŽ';chatSend('rag');return false">JP financiranje</a> ·
|
||
<a href="#" onclick="document.getElementById('askQ').value='NK Orijent Rijeka predsjednik';chatSend('rag');return false">NK Orijent</a> ·
|
||
<a href="#" onclick="document.getElementById('askQ').value='kotizacije za natjecanja u nogometu';chatSend('rag');return false">Kotizacije</a>
|
||
</div>
|
||
</div>`;
|
||
chatRender();
|
||
if (window.chatHistory.length === 0) {
|
||
const t = document.getElementById('chatThread');
|
||
if (t) t.innerHTML = '<div style="text-align:center;color:#788798;padding:40px;font-size:13px">💬 Postavi pitanje. Mogu odgovoriti glasovno (<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>) ili tekstom.</div>';
|
||
}
|
||
}
|
||
async function askGo() {
|
||
const q = document.getElementById('askQ').value.trim();
|
||
if (!q) return;
|
||
const out = document.getElementById('askOut');
|
||
out.innerHTML = '<div class="loader">Pretraga vektorske baze...</div>';
|
||
try {
|
||
const r = await fetch('/sport/api/v2/sport/ask', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({query:q, limit:10})});
|
||
const d = await r.json();
|
||
if (!d.results || !d.results.length) { out.innerHTML = '<div class="ban warn">Nema rezultata.</div>'; return; }
|
||
out.innerHTML = d.results.map(h => {
|
||
const url = (h.payload && (h.payload.source_url || h.payload.url)) || '';
|
||
const title = h.title || (h.payload && h.payload.title) || '(bez naslova)';
|
||
const tip = h.type || (h.payload && h.payload.tip) || '';
|
||
const klubId = h.payload && h.payload.klub_id;
|
||
const savezId = h.payload && h.payload.savez_id;
|
||
const docType = h.payload && h.payload.doc_type;
|
||
const sourceTag = h.payload && h.payload.source;
|
||
const publishDate = h.payload && h.payload.publish_date;
|
||
let click=''; let hint='';
|
||
if (url) { click='onclick="window.open(\''+url.replace(/\x27/g,'\\x27')+'\', \'_blank\')"'; hint='<span class="pill" style="background:#2a5e3a;color:#fff">otvori dokument</span>'; }
|
||
else if (klubId) { click='onclick="showKlub('+klubId+')"'; hint='<span class="pill ok">klub →</span>'; }
|
||
else if (savezId) { click='onclick="showSavez('+savezId+')"'; hint='<span class="pill ok">savez →</span>'; }
|
||
return '<div class="card" '+click+' style="margin-bottom:8px;cursor:'+(click?'pointer':'default')+';border-left:3px solid '+(h.score>0.7?'#27c79b':h.score>0.6?'#f0b429':'#7a7a7a')+'">'
|
||
+'<div style="display:flex;justify-content:space-between;align-items:start;gap:8px;flex-wrap:wrap">'
|
||
+'<div><b>'+title+'</b> <span class="pill">'+tip+'</span>'+(docType?' <span class="pill muted">'+docType+'</span>':'')+' '+hint+'</div>'
|
||
+'<div style="font-size:11px;color:var(--muted)">'+(publishDate?publishDate.slice(0,10)+' · ':'')+(sourceTag||'')+' · '+h.score.toFixed(3)+'</div>'
|
||
+'</div>'
|
||
+'<div style="margin-top:6px;font-size:13px;color:#bbb">'+(h.snippet||'').replace(/</g,'<').slice(0,400)+((h.snippet||'').length>400?'…':'')+'</div>'
|
||
+(url?'<div style="margin-top:4px;font-size:11px;color:#5e72e4;word-break:break-all">'+(url.length>120?url.slice(0,120)+'…':url)+'</div>':'')
|
||
+'</div>';
|
||
}).join('');
|
||
} catch(e) { out.innerHTML = '<div class="ban crit">Greška: '+e.message+'</div>'; }
|
||
}
|
||
|
||
|
||
async function pagePravnik() {
|
||
document.getElementById('content').innerHTML = `
|
||
<div class="page-h"><h2>AI Pravnik</h2><p class="muted">Stručni pravni odgovori temeljeni na pravilnicima HOO, MINT-a, ZSPGŽ-a, klubova i statuta. RAG + DeepSeek/Groq.</p></div>
|
||
<div class="card" style="margin-bottom:14px">
|
||
<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap">
|
||
<input id="lawQ" class="inp" style="flex:1;min-width:300px" placeholder="npr. Kako kategorizirati odbojkaša? Koji su rokovi za prijavu?" onkeydown="if(event.key==='Enter')lawGo()" />
|
||
<button class="v6-mic-btn" onclick="v6VoiceStart('lawQ', this)" title="Glasovni unos (hr-HR)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></button>
|
||
<button class="btn primary" onclick="lawGo()">Pitaj Pravnika</button>
|
||
</div>
|
||
<div style="font-size:12px;color:var(--muted)">Primjeri:
|
||
<a href="#" onclick="document.getElementById('lawQ').value='Kako kategorizirati odbojkaša prema HOO pravilniku?';lawGo();return false">Kategorizacija odbojkaša</a> ·
|
||
<a href="#" onclick="document.getElementById('lawQ').value='Koji su uvjeti za sufinanciranje sportskog programa iz proračuna PGŽ?';lawGo();return false">Sufinanciranje programa</a> ·
|
||
<a href="#" onclick="document.getElementById('lawQ').value='Što je potrebno za prijavu sportaša u registar?';lawGo();return false">Registar sportaša</a> ·
|
||
<a href="#" onclick="document.getElementById('lawQ').value='Koje su obveze kluba za godišnje izvješće?';lawGo();return false">Godišnje izvješće</a> ·
|
||
<a href="#" onclick="document.getElementById('lawQ').value='Koliko često sportaš mora obaviti liječnički pregled?';lawGo();return false">Liječnički pregledi</a>
|
||
</div>
|
||
</div>
|
||
<div id="lawOut"></div>`;
|
||
document.getElementById('lawQ').addEventListener('keypress', e => { if (e.key==='Enter') lawGo(); });
|
||
}
|
||
async function lawGo() {
|
||
const q = document.getElementById('lawQ').value.trim();
|
||
if (!q) return;
|
||
const out = document.getElementById('lawOut');
|
||
out.innerHTML = '<div class="loader">AI Pravnik analizira...</div>';
|
||
try {
|
||
const r = await fetch('/sport/api/v2/sport/lawyer', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({query:q})});
|
||
const d = await r.json();
|
||
if (d.detail) { out.innerHTML = '<div class="ban crit">'+d.detail+'</div>'; return; }
|
||
let html = `<div class="card" style="border-left:3px solid #5e72e4;margin-bottom:14px">
|
||
<div style="font-size:11px;color:var(--muted);margin-bottom:8px">Pitanje: ${q}</div>
|
||
<div style="font-size:12px;color:var(--muted);margin-bottom:8px">LLM: <b>${d.llm}</b> · ${d.hits_count||0} pravilnika · ${d.sources?.length||0} citata</div>
|
||
<div style="white-space:pre-wrap;line-height:1.6;font-size:14px;color:var(--text)">${(d.answer||'').replace(/</g,'<')}</div>
|
||
</div>`;
|
||
if (d.sources && d.sources.length) {
|
||
html += '<h3 style="margin:14px 0 8px">Reference</h3>';
|
||
d.sources.forEach((s,i) => {
|
||
const url = s.url || s.source_url || '';
|
||
const click = url ? 'onclick="window.open(\''+url.replace(/\x27/g,"\\x27")+'\',\'_blank\')" style="cursor:pointer"' : '';
|
||
html += `<div class="card" ${click} style="margin-bottom:6px;font-size:13px;transition:background 0.15s" onmouseover="this.style.background='#1a2330'" onmouseout="this.style.background=''">
|
||
<b>[${s.id}]</b> ${s.title} <span class="pill">${s.doc_type||'doc'}</span>
|
||
${url ? '<span class="pill" style="background:#2a5e3a;color:#fff">otvori dokument</span>' : ''}
|
||
<span style="float:right;color:var(--muted);font-size:11px">score ${(s.score||0).toFixed(3)}</span>
|
||
${url ? '<br><span style="font-size:11px;color:#5e72e4;word-break:break-all">'+(url.length>120?url.slice(0,120)+'…':url)+'</span>' : ''}
|
||
</div>`;
|
||
});
|
||
}
|
||
out.innerHTML = html;
|
||
} catch(e) { out.innerHTML = '<div class="ban crit">Greška: '+e.message+'</div>'; }
|
||
}
|
||
|
||
async function pageNatjecanja() {
|
||
document.getElementById('content').innerHTML = `
|
||
<div class="page-h"><h2>Natjecanja</h2><p class="muted">Sve lige i natjecanja klubova PGŽ — nogomet, košarka, rukomet, odbojka, vaterpolo i ostali sportovi.</p></div>
|
||
<div class="card" style="margin-bottom:12px">
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||
<select id="natSport" class="inp" onchange="loadNatj()">
|
||
<option value="">Svi sportovi</option>
|
||
</select>
|
||
<select id="natRazina" class="inp" onchange="loadNatj()">
|
||
<option value="">Sve razine</option>
|
||
<option value="zupanijski">Županijska (PGŽ)</option>
|
||
<option value="nacionalni">Nacionalna</option>
|
||
<option value="ostalo">Ostalo</option>
|
||
</select>
|
||
<input id="natQ" class="inp" placeholder="Pretraži..." onkeyup="loadNatj()" />
|
||
</div>
|
||
</div>
|
||
<div id="natList" class="loader">Učitavanje...</div>`;
|
||
await loadNatjFilters();
|
||
await loadNatj();
|
||
}
|
||
async function loadNatjFilters() {
|
||
try {
|
||
const r = await fetch('/sport/api/natjecanja/filters');
|
||
if (r.ok) {
|
||
const d = await r.json();
|
||
const sel = document.getElementById('natSport');
|
||
if (sel && d.sports) {
|
||
d.sports.forEach(s => sel.innerHTML += '<option value="'+s+'">'+s+'</option>');
|
||
}
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
async function loadNatj() {
|
||
const sport = document.getElementById('natSport').value;
|
||
const razina = document.getElementById('natRazina').value;
|
||
const q = document.getElementById('natQ').value.trim();
|
||
const out = document.getElementById('natList');
|
||
out.innerHTML = '<div class="loader">Učitavanje...</div>';
|
||
try {
|
||
let url = '/sport/api/natjecanja?limit=200';
|
||
if (sport) url += '&sport=' + encodeURIComponent(sport);
|
||
if (razina) url += '&razina=' + encodeURIComponent(razina);
|
||
if (q) url += '&q=' + encodeURIComponent(q);
|
||
const r = await fetch(url);
|
||
const d = await r.json();
|
||
if (!d.results || !d.results.length) { out.innerHTML = '<div class="empty">Nema natjecanja</div>'; return; }
|
||
let html = '<div style="margin-bottom:8px;color:var(--muted);font-size:12px">'+d.count+' natjecanja</div>';
|
||
html += '<table class="t"><tr><th>Sport</th><th>Razina</th><th>Naziv</th><th>Sezona</th><th>Kategorija</th><th>URL</th></tr>';
|
||
d.results.forEach(n => {
|
||
html += '<tr>'
|
||
+ '<td><span class="pill">'+(n.sport||'-')+'</span></td>'
|
||
+ '<td><span class="pill '+(n.razina==='zupanijski'?'ok':'muted')+'">'+(n.razina||'-')+'</span></td>'
|
||
+ '<td>'+(n.naziv||'-')+'</td>'
|
||
+ '<td>'+(n.sezona||'-')+'</td>'
|
||
+ '<td>'+(n.kategorija||'-')+'</td>'
|
||
+ '<td>'+(n.external_url ? '<a href="'+n.external_url+'" target="_blank" style="color:#5e72e4">otvori →</a>' : '-')+'</td>'
|
||
+ '</tr>';
|
||
});
|
||
html += '</table>';
|
||
out.innerHTML = html;
|
||
} catch(e) { out.innerHTML = '<div class="ban crit">'+e.message+'</div>'; }
|
||
}
|
||
|
||
async function pageAdmin() {
|
||
document.getElementById('content').innerHTML = `
|
||
<div class="page-h"><h2>Admin · System</h2><p class="muted">Upravljanje korisnicima portala. Multi-tenant po klubovima i savezima. Blockchain audit.</p></div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px">
|
||
<div class="card" id="adminStats">
|
||
<h3>Sažetak</h3>
|
||
<div id="adminStatsContent" class="loader">Učitavanje...</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>Novi korisnik</h3>
|
||
<div style="display:flex;flex-direction:column;gap:6px">
|
||
<input id="newEmail" class="inp" placeholder="Email" />
|
||
<div style="display:flex;gap:6px">
|
||
<input id="newIme" class="inp" placeholder="Ime" style="flex:1" />
|
||
<input id="newPrezime" class="inp" placeholder="Prezime" style="flex:1" />
|
||
</div>
|
||
<select id="newType" class="inp">
|
||
<option value="pgz_admin">pgz_admin (PGŽ Odjel sporta — sve)</option>
|
||
<option value="pgz_user">pgz_user (PGŽ Odjel sporta — pregled)</option>
|
||
<option value="pgz_finance">pgz_finance (PGŽ Finance)</option>
|
||
<option value="pgz_zzjz">pgz_zzjz (ZZJZ medical)</option>
|
||
<option value="savez_admin">savez_admin (Tajnik saveza)</option>
|
||
<option value="savez_user">savez_user (Pomoćnik saveza)</option>
|
||
<option value="klub_admin">klub_admin (Tajnik kluba)</option>
|
||
<option value="klub_user">klub_user (Pomoćnik kluba)</option>
|
||
<option value="klub_clan">klub_clan (Sportaš)</option>
|
||
</select>
|
||
<input id="newKlubId" class="inp" placeholder="klub_id (opcionalno)" />
|
||
<input id="newSavezId" class="inp" placeholder="savez_id (opcionalno)" />
|
||
<input id="newPwd" class="inp" type="password" placeholder="Privremeni password" />
|
||
<button class="btn primary" onclick="createUser()">➕ Stvori korisnika</button>
|
||
<div id="newUserStatus" style="font-size:12px;color:var(--muted)"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="card" style="margin-bottom:14px">
|
||
<h3>Svi korisnici</h3>
|
||
<div style="display:flex;gap:8px;margin-bottom:8px">
|
||
<input id="adminQ" class="inp" placeholder="Pretraži po emailu/imenu" onkeyup="loadUsers()" style="flex:1" />
|
||
<select id="adminFilterType" class="inp" onchange="loadUsers()">
|
||
<option value="">Svi tipovi</option>
|
||
<option value="pgz_admin">pgz_admin</option>
|
||
<option value="pgz_user">pgz_user</option>
|
||
<option value="pgz_finance">pgz_finance</option>
|
||
<option value="pgz_zzjz">pgz_zzjz</option>
|
||
<option value="savez_admin">savez_admin</option>
|
||
<option value="klub_admin">klub_admin</option>
|
||
<option value="klub_user">klub_user</option>
|
||
<option value="klub_clan">klub_clan</option>
|
||
</select>
|
||
</div>
|
||
<div id="usersList" class="loader">Učitavanje...</div>
|
||
</div>
|
||
<div class="card" style="margin-bottom:14px">
|
||
<h3>Multi-tenant veze</h3>
|
||
<p class="muted" style="font-size:12px">Korisnik može imati prava na više klubova/saveza s različitim ulogama (tajnik, predsjednik, sportaš, trener...)</p>
|
||
<div style="display:flex;gap:8px;margin-bottom:8px;flex-wrap:wrap">
|
||
<input id="linkUserId" class="inp" placeholder="user_id" type="number" />
|
||
<input id="linkKlubId" class="inp" placeholder="klub_id" type="number" />
|
||
<input id="linkSavezId" class="inp" placeholder="ili savez_id" type="number" />
|
||
<select id="linkRole" class="inp">
|
||
<option value="tajnik">Tajnik</option>
|
||
<option value="predsjednik">Predsjednik</option>
|
||
<option value="clan_uprave">Član uprave</option>
|
||
<option value="trener">Trener</option>
|
||
<option value="sportas">Sportaš</option>
|
||
<option value="volonter">Volonter</option>
|
||
<option value="clan">Član</option>
|
||
</select>
|
||
<button class="btn primary" onclick="createKlubLink()">🔗 Dodaj vezu</button>
|
||
</div>
|
||
<div id="klubLinksList" class="loader">Učitavanje...</div>
|
||
</div>
|
||
<div class="card" style="margin-bottom:14px">
|
||
<h3>Audit Log <span class="pill" id="chainStatus">verifying...</span></h3>
|
||
<p class="muted" style="font-size:12px">Sve akcije u sustavu zapisuju se u hash-chained ledger. Svaka izmjena uvjetuje potpis prethodnog reda. Ne može se neopaženo izmijeniti.</p>
|
||
<div style="display:flex;gap:8px;margin-bottom:8px">
|
||
<input id="auditQ" class="inp" placeholder="Filter po akciji" onkeyup="loadAuditChain()" style="flex:1" />
|
||
<button class="btn" onclick="verifyChain()">🔍 Verify Chain</button>
|
||
<button class="btn" onclick="loadAuditChain()">↻ Refresh</button>
|
||
</div>
|
||
<div id="auditList" class="loader">Učitavanje...</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>Permission matrica</h3>
|
||
<div id="permMatrix" class="loader">Učitavanje...</div>
|
||
</div>`;
|
||
await loadAdminStats();
|
||
await loadUsers();
|
||
await loadKlubLinks();
|
||
await loadAuditChain();
|
||
await verifyChain();
|
||
await loadPermMatrix();
|
||
}
|
||
async function loadKlubLinks() {
|
||
try {
|
||
const r = await fetch('/sport/api/admin/klub-links');
|
||
const d = await r.json();
|
||
let html = '<table class="t" style="font-size:12px"><tr><th>ID</th><th>Korisnik</th><th>Klub/Savez</th><th>Uloga</th><th>Aktivan</th><th>Od</th><th>Akcije</th></tr>';
|
||
(d.results||[]).forEach(l => {
|
||
const subj = l.klub_naziv || l.savez_naziv || '?';
|
||
const subjType = l.klub_id ? '<span class="pill ok">klub</span>' : '<span class="pill info">savez</span>';
|
||
html += '<tr>'
|
||
+ '<td>'+l.id+'</td>'
|
||
+ '<td>'+(l.email||'?')+'<br><span class="muted" style="font-size:10px">'+(l.ime||'')+' '+(l.prezime||'')+'</span></td>'
|
||
+ '<td>'+subjType+' '+subj+' (#'+(l.klub_id||l.savez_id)+')</td>'
|
||
+ '<td><span class="pill">'+l.role+'</span>'+(l.primary_link?' <span class="pill ok">primary</span>':'')+'</td>'
|
||
+ '<td>'+(l.aktivan ? '✅' : '❌')+'</td>'
|
||
+ '<td style="font-size:10px">'+(l.granted_at||'').slice(0,10)+'</td>'
|
||
+ '<td><a href="#" onclick="deleteKlubLink('+l.id+');return false">✕</a></td>'
|
||
+ '</tr>';
|
||
});
|
||
if (!(d.results||[]).length) html += '<tr><td colspan="7" style="text-align:center;color:var(--muted)">Nema veza</td></tr>';
|
||
html += '</table>';
|
||
document.getElementById('klubLinksList').innerHTML = html;
|
||
} catch (e) { document.getElementById('klubLinksList').innerHTML = '<div class="ban crit">'+e.message+'</div>'; }
|
||
}
|
||
async function createKlubLink() {
|
||
const body = {
|
||
user_id: parseInt(document.getElementById('linkUserId').value),
|
||
klub_id: parseInt(document.getElementById('linkKlubId').value)||null,
|
||
savez_id: parseInt(document.getElementById('linkSavezId').value)||null,
|
||
role: document.getElementById('linkRole').value
|
||
};
|
||
if (!body.user_id || (!body.klub_id && !body.savez_id)) { alert('user_id + (klub_id ili savez_id) obavezno'); return; }
|
||
try {
|
||
const r = await fetch('/sport/api/admin/klub-links', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)});
|
||
const d = await r.json();
|
||
if (d.id) {
|
||
document.getElementById('linkUserId').value='';
|
||
document.getElementById('linkKlubId').value='';
|
||
document.getElementById('linkSavezId').value='';
|
||
loadKlubLinks();
|
||
} else alert(d.detail||'greška');
|
||
} catch (e) { alert(e.message); }
|
||
}
|
||
async function deleteKlubLink(id) {
|
||
if (!confirm('Obrisati vezu #'+id+'?')) return;
|
||
try { await fetch('/sport/api/admin/klub-links/'+id, {method:'DELETE'}); loadKlubLinks(); } catch (e) { alert(e.message); }
|
||
}
|
||
async function loadAuditChain() {
|
||
const q = (document.getElementById('auditQ')||{value:''}).value;
|
||
let url = '/sport/api/admin/audit-chain?limit=50';
|
||
if (q) url += '&action='+encodeURIComponent(q);
|
||
try {
|
||
const r = await fetch(url);
|
||
const d = await r.json();
|
||
let html = '<table class="t" style="font-size:11px"><tr><th>#</th><th>Vrijeme</th><th>Akcija</th><th>Target</th><th>User</th><th>Hash</th><th>Prev</th></tr>';
|
||
d.forEach(a => {
|
||
html += '<tr>'
|
||
+ '<td>'+a.chain_idx+'</td>'
|
||
+ '<td style="white-space:nowrap">'+(a.created_at||'').slice(0,16).replace('T',' ')+'</td>'
|
||
+ '<td><b>'+a.action+'</b></td>'
|
||
+ '<td>'+(a.target_type||'-')+(a.target_id?' #'+a.target_id:'')+(a.target_text?'<br><span class="muted" style="font-size:10px">'+a.target_text.slice(0,80)+'</span>':'')+'</td>'
|
||
+ '<td>'+(a.user_email||'system')+'</td>'
|
||
+ '<td><code style="font-size:10px;color:#27c79b">'+a.row_hash+'</code></td>'
|
||
+ '<td><code style="font-size:10px;color:var(--muted)">'+a.prev_hash+'</code></td>'
|
||
+ '</tr>';
|
||
});
|
||
html += '</table>';
|
||
if (!d.length) html = '<div class="empty">Nema audit zapisa</div>';
|
||
document.getElementById('auditList').innerHTML = html;
|
||
} catch(e) { document.getElementById('auditList').innerHTML = '<div class="ban crit">'+e.message+'</div>'; }
|
||
}
|
||
async function verifyChain() {
|
||
document.getElementById('chainStatus').innerHTML = 'verifying...';
|
||
try {
|
||
const r = await fetch('/sport/api/admin/audit-chain/verify');
|
||
const d = await r.json();
|
||
if (d.valid) {
|
||
document.getElementById('chainStatus').innerHTML = '<span style="color:#27c79b">OK</span> · '+d.total_rows+' rows · last hash: '+(d.last_hash||'').slice(0,16)+'…';
|
||
document.getElementById('chainStatus').className = 'pill ok';
|
||
} else {
|
||
document.getElementById('chainStatus').innerHTML = '<span style="color:#e74c3c">BROKEN at chain_idx '+d.broken_at.chain_idx+'</span>';
|
||
document.getElementById('chainStatus').className = 'pill crit';
|
||
}
|
||
} catch(e) { document.getElementById('chainStatus').innerHTML = 'err: '+e.message; }
|
||
}
|
||
|
||
async function loadAdminStats() {
|
||
try {
|
||
const r = await fetch('/sport/api/admin/stats');
|
||
const d = await r.json();
|
||
document.getElementById('adminStatsContent').innerHTML = `
|
||
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:8px">
|
||
<div><b>${d.users_total||0}</b><br><span class="muted" style="font-size:11px">korisnika</span></div>
|
||
<div><b>${d.users_active||0}</b><br><span class="muted" style="font-size:11px">aktivnih</span></div>
|
||
<div><b>${d.permissions_total||0}</b><br><span class="muted" style="font-size:11px">dozvola</span></div>
|
||
<div><b>${d.audit_today||0}</b><br><span class="muted" style="font-size:11px">akcija danas</span></div>
|
||
</div>
|
||
<h4 style="margin-top:14px;margin-bottom:6px">Po tipu korisnika</h4>
|
||
<table class="t" style="font-size:12px">
|
||
${(d.by_type||[]).map(r => '<tr><td>'+r.user_type+'</td><td><b>'+r.cnt+'</b></td></tr>').join('')}
|
||
</table>`;
|
||
} catch(e) { document.getElementById('adminStatsContent').innerHTML = '<div class="ban crit">'+e.message+'</div>'; }
|
||
}
|
||
async function loadUsers() {
|
||
const q = document.getElementById('adminQ').value.trim();
|
||
const tp = document.getElementById('adminFilterType').value;
|
||
let url = '/sport/api/admin/users?limit=100';
|
||
if (q) url += '&q='+encodeURIComponent(q);
|
||
if (tp) url += '&user_type='+tp;
|
||
try {
|
||
const r = await fetch(url);
|
||
const d = await r.json();
|
||
let html = '<table class="t"><tr><th>ID</th><th>Email</th><th>Ime</th><th>Tip</th><th>Klub</th><th>Savez</th><th>Aktivan</th><th>Akcije</th></tr>';
|
||
(d.results || []).forEach(u => {
|
||
html += '<tr>'
|
||
+ '<td>'+u.id+'</td>'
|
||
+ '<td><b>'+u.email+'</b></td>'
|
||
+ '<td>'+(u.ime||'')+' '+(u.prezime||'')+'</td>'
|
||
+ '<td><span class="pill">'+u.user_type+'</span></td>'
|
||
+ '<td>'+(u.klub_id||'-')+'</td>'
|
||
+ '<td>'+(u.savez_id||'-')+'</td>'
|
||
+ '<td>'+(u.aktivan ? '✅' : '❌')+'</td>'
|
||
+ '<td><a href="#" onclick="editUser('+u.id+');return false">edit</a> · <a href="#" onclick="toggleUser('+u.id+');return false">toggle</a></td>'
|
||
+ '</tr>';
|
||
});
|
||
html += '</table>';
|
||
document.getElementById('usersList').innerHTML = html;
|
||
} catch(e) { document.getElementById('usersList').innerHTML = '<div class="ban crit">'+e.message+'</div>'; }
|
||
}
|
||
async function loadPermMatrix() {
|
||
try {
|
||
const r = await fetch('/sport/api/admin/permissions-matrix');
|
||
const d = await r.json();
|
||
let html = '<div style="overflow-x:auto"><table class="t" style="font-size:11px"><tr><th>Kategorija</th><th>Permission</th>';
|
||
const types = d.user_types || [];
|
||
types.forEach(t => html += '<th>'+t+'</th>');
|
||
html += '</tr>';
|
||
(d.matrix || []).forEach(row => {
|
||
html += '<tr><td>'+row.kategorija+'</td><td><b>'+row.code+'</b><br><span class="muted">'+row.naziv+'</span></td>';
|
||
types.forEach(t => {
|
||
const has = (row.granted_to || []).includes(t);
|
||
html += '<td style="text-align:center">'+(has ? '✅' : '–')+'</td>';
|
||
});
|
||
html += '</tr>';
|
||
});
|
||
html += '</table></div>';
|
||
document.getElementById('permMatrix').innerHTML = html;
|
||
} catch(e) { document.getElementById('permMatrix').innerHTML = '<div class="ban crit">'+e.message+'</div>'; }
|
||
}
|
||
async function createUser() {
|
||
const body = {
|
||
email: document.getElementById('newEmail').value.trim(),
|
||
ime: document.getElementById('newIme').value.trim(),
|
||
prezime: document.getElementById('newPrezime').value.trim(),
|
||
user_type: document.getElementById('newType').value,
|
||
klub_id: parseInt(document.getElementById('newKlubId').value) || null,
|
||
savez_id: parseInt(document.getElementById('newSavezId').value) || null,
|
||
password: document.getElementById('newPwd').value,
|
||
};
|
||
if (!body.email || !body.password) { alert('Email + password su obavezni'); return; }
|
||
try {
|
||
const r = await fetch('/sport/api/admin/users', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)});
|
||
const d = await r.json();
|
||
if (d.id) {
|
||
document.getElementById('newUserStatus').innerHTML = '✅ Stvoren korisnik #'+d.id;
|
||
document.getElementById('newEmail').value = '';
|
||
document.getElementById('newIme').value = '';
|
||
document.getElementById('newPrezime').value = '';
|
||
document.getElementById('newPwd').value = '';
|
||
loadUsers(); loadAdminStats();
|
||
} else {
|
||
document.getElementById('newUserStatus').innerHTML = '❌ ' + (d.detail || 'greška');
|
||
}
|
||
} catch(e) { document.getElementById('newUserStatus').innerHTML = '❌ '+e.message; }
|
||
}
|
||
async function toggleUser(id) {
|
||
if (!confirm('Toggle aktivan status korisnika #'+id+'?')) return;
|
||
try {
|
||
await fetch('/sport/api/admin/users/'+id+'/toggle', {method:'POST'});
|
||
loadUsers();
|
||
} catch(e) { alert(e.message); }
|
||
}
|
||
function editUser(id) { alert('Edit user '+id+' — TODO: full edit modal'); }
|
||
|
||
async function pageInvoices() {
|
||
document.getElementById('content').innerHTML = `
|
||
<div class="page-h"><h2>Računi · ERP</h2><p class="muted">Upload računa s OCR-om · IFRS knjigovodstvo · obveze/potraživanja</p></div>
|
||
<div class="card" style="margin-bottom:12px">
|
||
<h3>Upload računa (PDF/JPG/PNG)</h3>
|
||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||
<input type="file" id="invFile" accept=".pdf,.jpg,.jpeg,.png" />
|
||
<select id="invKind" class="inp"><option value="ulazni">Ulazni</option><option value="izlazni">Izlazni</option></select>
|
||
<input id="invKlub" class="inp" placeholder="klub_id (npr. 524)" />
|
||
<button class="btn primary" onclick="uploadInvoice()">Upload + OCR</button>
|
||
</div>
|
||
<div id="invUpStatus" style="margin-top:8px;font-size:13px"></div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>OCR red + Uneseni računi</h3>
|
||
<div id="invList" class="loader">Učitavanje…</div>
|
||
</div>`;
|
||
await loadInvoices();
|
||
}
|
||
async function loadInvoices() {
|
||
try {
|
||
const ups = await v2Fetch('/invoice-uploads?limit=20');
|
||
const invs = await v2Fetch('/invoices?limit=20');
|
||
let html = '';
|
||
if (ups.length) {
|
||
html += '<h4 style="margin-top:0">📥 OCR red ('+ups.length+')</h4><table class="t"><tr><th>Datum</th><th>Status</th><th>Vendor</th><th>Iznos</th><th>Br.</th><th>Akcija</th></tr>';
|
||
ups.forEach(u => html += `<tr>
|
||
<td>${(u.uploaded_at||'').slice(0,16).replace('T',' ')}</td>
|
||
<td><span class="pill ${u.ocr_status==='done'?'ok':u.ocr_status==='failed'?'crit':'warn'}">${u.ocr_status}</span></td>
|
||
<td>${u.ai_vendor_name||'–'}</td>
|
||
<td>${u.ai_amount_gross?u.ai_amount_gross+' EUR':'–'}</td>
|
||
<td>${u.ai_invoice_no||'–'}</td>
|
||
<td><a href="#" onclick="detailUpload(${u.id});return false">detalji</a></td></tr>`);
|
||
html += '</table>';
|
||
}
|
||
if (invs.length) {
|
||
html += '<h4>📑 Uneseni računi ('+invs.length+')</h4><table class="t"><tr><th>Br.</th><th>Datum</th><th>Vendor</th><th>Iznos</th><th>Status</th></tr>';
|
||
invs.forEach(i => html += `<tr><td>${i.invoice_no||'–'}</td><td>${(i.invoice_date||'').slice(0,10)}</td><td>${i.vendor_name||'–'}</td><td>${i.amount_gross} ${i.currency}</td><td><span class="pill ${i.payment_status==='paid'?'ok':'warn'}">${i.payment_status}</span></td></tr>`);
|
||
html += '</table>';
|
||
}
|
||
if (!html) html = '<div class="muted">Nema računa. Upload prvi.</div>';
|
||
document.getElementById('invList').innerHTML = html;
|
||
} catch(e) { document.getElementById('invList').innerHTML = '<div class="ban crit">Login potreban: '+e.message+'</div>'; }
|
||
}
|
||
async function uploadInvoice() {
|
||
const f = document.getElementById('invFile').files[0];
|
||
const kind = document.getElementById('invKind').value;
|
||
let klub_id = (typeof getPickerKlubId === 'function') ? getPickerKlubId('invKlub') : 0;
|
||
if (!klub_id) {
|
||
// Fallback - try parse from raw value
|
||
const v = (document.getElementById('invKlub') || {}).value || '';
|
||
const m = v.match(/#(\d+)/);
|
||
klub_id = m ? parseInt(m[1]) : parseInt(v) || 0;
|
||
}
|
||
if (!f) { alert('Odaberi datoteku'); return; }
|
||
if (!klub_id) { alert('Klub nije izabran. Upiši ime kluba (npr. Zamet) i klikni iz padajuce liste'); return; }
|
||
const tok = localStorage.getItem('rinet_v2_token');
|
||
if (!tok) { alert('Login potreban'); return; }
|
||
const fd = new FormData();
|
||
fd.append('file', f); fd.append('klub_id', klub_id); fd.append('invoice_kind', kind);
|
||
document.getElementById('invUpStatus').innerHTML = '⏳ Upload + OCR queue…';
|
||
try {
|
||
const r = await fetch('/sport/api/v2/invoice-uploads/file', {method:'POST', headers:{Authorization:'Bearer '+tok}, body: fd});
|
||
const d = await r.json();
|
||
if (!r.ok) throw new Error(d.detail||r.status);
|
||
document.getElementById('invUpStatus').innerHTML = `✅ ID ${d.upload_id} u OCR redu (${d.ocr_status})`;
|
||
setTimeout(loadInvoices, 1500);
|
||
} catch(e) { document.getElementById('invUpStatus').innerHTML = '❌ '+e.message; }
|
||
}
|
||
async function detailUpload(id) {
|
||
let bd = document.querySelector('.ri-modal');
|
||
if (bd) bd.remove();
|
||
bd = document.createElement('div');
|
||
bd.className = 'ri-modal';
|
||
bd.id = 'riModal';
|
||
bd.onclick = (e) => { if (e.target === bd) closeRiModal(); };
|
||
bd.innerHTML = `<div class="ri-modal-box" style="max-width:800px">
|
||
<div class="ri-modal-h"><b>Detalji uploada #${id}</b>
|
||
<button class="ri-icon-btn" onclick="closeRiModal()">${iconX()}</button></div>
|
||
<div class="ri-modal-body" id="invDetailBody" style="padding:20px"><div class="muted">Učitavam...</div></div>
|
||
</div>`;
|
||
document.body.appendChild(bd);
|
||
|
||
try {
|
||
const r = await fetch('/sport/api/v2/invoice-uploads/' + id, {
|
||
headers: {'Authorization':'Bearer '+(localStorage.getItem('rinet_v2_token')||'')}
|
||
});
|
||
if (!r.ok) throw new Error('API ' + r.status);
|
||
const d = await r.json();
|
||
const body = document.getElementById('invDetailBody');
|
||
let h = '<table class="ri-tbl"><tbody>';
|
||
for (const [k, v] of Object.entries(d)) {
|
||
if (v === null || v === undefined) continue;
|
||
let val = v;
|
||
if (k === 'extracted_json' && typeof v === 'object') {
|
||
val = '<pre style="white-space:pre-wrap;font-size:11px;background:var(--bg-2);padding:8px;border-radius:4px;max-height:300px;overflow:auto">' + JSON.stringify(v, null, 2) + '</pre>';
|
||
} else if (typeof v === 'object') {
|
||
val = '<pre style="font-size:11px">' + JSON.stringify(v) + '</pre>';
|
||
} else {
|
||
val = String(v).replace(/</g,'<');
|
||
}
|
||
h += `<tr><td style="font-weight:600;width:30%;color:var(--text3)">${k}</td><td>${val}</td></tr>`;
|
||
}
|
||
h += '</tbody></table>';
|
||
body.innerHTML = h;
|
||
} catch(e) {
|
||
document.getElementById('invDetailBody').innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>';
|
||
}
|
||
}
|
||
|
||
async function pageExpenses() {
|
||
document.getElementById('content').innerHTML = `
|
||
<div class="page-h"><h2>Putni nalozi & obračuni</h2><p class="muted">0,50 EUR/km vlastiti auto (Pravilnik NN 143/23) · 30 EUR/dnevnica HR</p></div>
|
||
<div class="card" style="margin-bottom:12px">
|
||
<h3>Novi putni nalog</h3>
|
||
<div class="grid" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px">
|
||
<input id="exKlub" class="inp" placeholder="klub_id" />
|
||
<select id="exType" class="inp">
|
||
<option value="putni_nalog">Putni nalog</option>
|
||
<option value="vlastiti_auto">Vlastiti auto</option>
|
||
<option value="dnevnice">Dnevnice</option>
|
||
</select>
|
||
<input id="exDest" class="inp" placeholder="Destinacija (npr. Zagreb)" />
|
||
<input id="exFrom" class="inp" type="date" />
|
||
<input id="exTo" class="inp" type="date" />
|
||
<input id="exKm" class="inp" type="number" placeholder="km (vlastiti auto)" />
|
||
<input id="exDni" class="inp" type="number" placeholder="dani dnevnica" />
|
||
<input id="exTransp" class="inp" type="number" placeholder="trošak prijevoza EUR" />
|
||
<input id="exHotel" class="inp" type="number" placeholder="trošak smještaja EUR" />
|
||
</div>
|
||
<button class="btn primary" style="margin-top:8px" onclick="saveExpense()">💾 Spremi</button>
|
||
<div id="exStatus" style="margin-top:8px"></div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>Postojeći obračuni</h3>
|
||
<div id="exList" class="loader">Učitavanje…</div>
|
||
</div>`;
|
||
await loadExpenses();
|
||
}
|
||
async function loadExpenses() {
|
||
try {
|
||
const ex = await v2Fetch('/expense-reports?limit=20');
|
||
if (!ex.length) { document.getElementById('exList').innerHTML = '<div class="muted">Nema obračuna.</div>'; return; }
|
||
let html = '<table class="t"><tr><th>Datum</th><th>Tip</th><th>Destinacija</th><th>km</th><th>Ukupno</th><th>Status</th></tr>';
|
||
ex.forEach(e => html += `<tr><td>${(e.date_from||'').slice(0,10)}</td><td>${e.report_type}</td><td>${e.destination||'–'}</td><td>${e.km_driven||0}</td><td><b>${e.cost_total} EUR</b></td><td><span class="pill">${e.status}</span></td></tr>`);
|
||
html += '</table>';
|
||
document.getElementById('exList').innerHTML = html;
|
||
} catch(e) { document.getElementById('exList').innerHTML = '<div class="ban crit">Login potreban: '+e.message+'</div>'; }
|
||
}
|
||
async function saveExpense() {
|
||
const body = {
|
||
klub_id: parseInt(document.getElementById('exKlub').value||'0'),
|
||
report_type: document.getElementById('exType').value,
|
||
destination: document.getElementById('exDest').value,
|
||
date_from: document.getElementById('exFrom').value,
|
||
date_to: document.getElementById('exTo').value,
|
||
km_driven: parseFloat(document.getElementById('exKm').value||'0'),
|
||
dnevnice_count: parseInt(document.getElementById('exDni').value||'0'),
|
||
cost_transport: parseFloat(document.getElementById('exTransp').value||'0'),
|
||
cost_lodging: parseFloat(document.getElementById('exHotel').value||'0')
|
||
};
|
||
if (!body.klub_id || !body.date_from) { alert('Unesi klub i datum.'); return; }
|
||
try {
|
||
const d = await v2Fetch('/expense-reports', {method:'POST', body: JSON.stringify(body)});
|
||
document.getElementById('exStatus').innerHTML = `✅ Obračun #${d.report_id} | total ${d.cost_total} EUR`;
|
||
setTimeout(loadExpenses, 800);
|
||
} catch(e) { document.getElementById('exStatus').innerHTML = '❌ '+e.message; }
|
||
}
|
||
|
||
async function pageForms() {
|
||
document.getElementById('content').innerHTML = `
|
||
<div class="page-h"><h2>Obrasci</h2><p class="muted">8 templatea · prijave · sufinanciranje · liječnički · putni · godišnji</p></div>
|
||
<div id="formsList" class="loader">Učitavanje…</div>
|
||
<div id="formRender"></div>`;
|
||
try {
|
||
const tpls = await v2Fetch('/forms/templates');
|
||
document.getElementById('formsList').innerHTML = '<div class="grid" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:10px">' +
|
||
tpls.map(t => `<div class="card" style="cursor:pointer" onclick="openForm('${t.code}')">
|
||
<h4 style="margin:0 0 4px 0">${t.naziv}</h4>
|
||
<div class="muted" style="font-size:12px">${t.kategorija} · za: ${t.required_role||'svi'}</div>
|
||
<div style="margin-top:6px;font-size:12px">${t.field_count||(t.schema_json&&t.schema_json.fields?t.schema_json.fields.length:'?')} polja</div>
|
||
</div>`).join('') + '</div>';
|
||
} catch(e) { document.getElementById('formsList').innerHTML = '<div class="ban crit">'+e.message+'</div>'; }
|
||
}
|
||
async function openForm(code) {
|
||
try {
|
||
const tpl = await v2Fetch('/forms/templates/'+code);
|
||
let fields = (tpl.schema_json && tpl.schema_json.fields) || [];
|
||
|
||
// V6: Smart layout — group fields into 2-3 columns based on type/length
|
||
function colSpan(f) {
|
||
if (f.type === 'textarea' || f.name === 'napomena' || f.name === 'opis') return 2;
|
||
if (f.type === 'file') return 2;
|
||
if (f.name && (f.name.includes('adresa') || f.name.includes('napomena'))) return 2;
|
||
return 1;
|
||
}
|
||
|
||
// V6: Detect special form types
|
||
const isPutni = code === 'obracun_putnih_troskova' || code === 'putni_nalog';
|
||
const isSportas = code === 'prijava_sportasa';
|
||
|
||
let html = '<div class="v6-form" id="v6Form">';
|
||
html += ' <div class="v6-fh"><h3>📋 ' + tpl.naziv + '</h3>';
|
||
html += ' <div class="v6-actions">';
|
||
html += ' <button class="v6-btn" onclick="document.getElementById(\'formRender\').innerHTML=\'\'">Zatvori</button>';
|
||
html += ' </div></div>';
|
||
|
||
if (isPutni) {
|
||
// V6 PUTNI NALOG: Special AI-powered layout
|
||
html += ' <div class="v6-fs">';
|
||
html += ' <div class="v6-fs-t">Putovanje</div>';
|
||
html += ' <div class="v6-g3">';
|
||
html += ' <div class="v6-fld v6-ac">';
|
||
html += ' <label class="v6-lbl req">Polazište</label>';
|
||
html += ' <input id="ff_polaziste" class="v6-inp" type="text" placeholder="npr. Rijeka" oninput="v6GradAuto(this,\'polaziste\')" onchange="v6CalcKM()" />';
|
||
html += ' <div id="ac_polaziste" class="v6-ac-s"></div>';
|
||
html += ' </div>';
|
||
html += ' <div class="v6-fld v6-ac">';
|
||
html += ' <label class="v6-lbl req">Odredište</label>';
|
||
html += ' <input id="ff_odrediste" class="v6-inp" type="text" placeholder="npr. Zagreb" oninput="v6GradAuto(this,\'odrediste\')" onchange="v6CalcKM()" />';
|
||
html += ' <div id="ac_odrediste" class="v6-ac-s"></div>';
|
||
html += ' </div>';
|
||
html += ' <div class="v6-fld">';
|
||
html += ' <label class="v6-lbl">🤖 AI udaljenost (jedan pravac)</label>';
|
||
html += ' <input id="ff_ai_km" class="v6-inp v6-num v6-calc" type="number" step="0.1" readonly />';
|
||
html += ' </div>';
|
||
html += ' </div>';
|
||
html += ' <div id="aiHint" style="font-size:11px;color:#5e72e4;margin-top:6px"></div>';
|
||
html += ' </div>';
|
||
|
||
html += ' <div class="v6-fs">';
|
||
html += ' <div class="v6-fs-t">Datumi i kilometraža</div>';
|
||
html += ' <div class="v6-g4">';
|
||
html += ' <div class="v6-fld"><label class="v6-lbl req">Datum/sat polaska</label><input id="ff_datum_polaska" class="v6-inp" type="datetime-local" /></div>';
|
||
html += ' <div class="v6-fld"><label class="v6-lbl req">Datum/sat povratka</label><input id="ff_datum_povratka" class="v6-inp" type="datetime-local" /></div>';
|
||
html += ' <div class="v6-fld"><label class="v6-lbl">KM stanje na polasku</label><input id="ff_km_pre" class="v6-inp v6-num" type="number" oninput="v6CalcKM()" /></div>';
|
||
html += ' <div class="v6-fld"><label class="v6-lbl">KM stanje na povratku</label><input id="ff_km_post" class="v6-inp v6-num" type="number" oninput="v6CalcKM()" /></div>';
|
||
html += ' </div>';
|
||
html += ' <div class="v6-g4" style="margin-top:8px">';
|
||
html += ' <div class="v6-fld"><label class="v6-lbl req">Ukupno prijeđeno KM <span class="v6-pill">auto: 2× pravac ili stanje povratka − stanje polaska</span></label><input id="ff_km_total" class="v6-inp v6-num v6-calc" type="number" step="0.1" oninput="v6CalcCost()" /></div>';
|
||
html += ' <div class="v6-fld"><label class="v6-lbl">Cijena po KM (EUR)</label><input id="ff_cijena_km" class="v6-inp v6-num" type="number" step="0.01" value="0.50" oninput="v6CalcCost()" /></div>';
|
||
html += ' <div class="v6-fld v6-w2"><label class="v6-lbl">💰 Trošak prijevoza (auto)</label><input id="ff_trosak_prijevoz" class="v6-inp v6-num v6-calc" type="number" step="0.01" readonly /></div>';
|
||
html += ' </div>';
|
||
html += ' </div>';
|
||
|
||
// OCR Attachments
|
||
html += ' <div class="v6-fs">';
|
||
html += ' <div class="v6-fs-t">Prilozi · OCR</div>';
|
||
html += ' <div class="v6-att-z" onclick="document.getElementById(\'v6FilePick\').click()">';
|
||
html += ' <input type="file" id="v6FilePick" accept=".pdf,.jpg,.jpeg,.png" multiple style="display:none" onchange="v6UploadPrilog(this)" />';
|
||
html += ' <div>Klikni ili dovuci PDF/JPG/PNG (cestarine, gorivo, parking, smještaj)</div>';
|
||
html += ' <div style="font-size:11px;color:#788798;margin-top:4px">AI OCR će automatski pročitati iznos, datum, dobavljača, OIB</div>';
|
||
html += ' </div>';
|
||
html += ' <div id="v6AttList" class="v6-att-l"></div>';
|
||
html += ' </div>';
|
||
|
||
// Costs grid
|
||
html += ' <div class="v6-fs">';
|
||
html += ' <div class="v6-fs-t">Troškovi (EUR)</div>';
|
||
html += ' <div class="v6-g4">';
|
||
html += ' <div class="v6-fld"><label class="v6-lbl">Cestarine</label><input id="ff_cestarine" class="v6-inp v6-num" type="number" step="0.01" value="0" oninput="v6CalcTotal()" /></div>';
|
||
html += ' <div class="v6-fld"><label class="v6-lbl">Parkirne</label><input id="ff_parkirne" class="v6-inp v6-num" type="number" step="0.01" value="0" oninput="v6CalcTotal()" /></div>';
|
||
html += ' <div class="v6-fld"><label class="v6-lbl">Gorivo</label><input id="ff_gorivo" class="v6-inp v6-num" type="number" step="0.01" value="0" oninput="v6CalcTotal()" /></div>';
|
||
html += ' <div class="v6-fld"><label class="v6-lbl">Smještaj</label><input id="ff_smjestaj" class="v6-inp v6-num" type="number" step="0.01" value="0" oninput="v6CalcTotal()" /></div>';
|
||
html += ' </div>';
|
||
html += ' <div class="v6-g4" style="margin-top:8px">';
|
||
html += ' <div class="v6-fld"><label class="v6-lbl">Ostali troškovi</label><input id="ff_ostali" class="v6-inp v6-num" type="number" step="0.01" value="0" oninput="v6CalcTotal()" /></div>';
|
||
html += ' <div class="v6-fld"><label class="v6-lbl">Broj dnevnica</label><input id="ff_dnevnice_n" class="v6-inp v6-num" type="number" value="0" oninput="v6CalcTotal()" /></div>';
|
||
html += ' <div class="v6-fld"><label class="v6-lbl">Iznos po dnevnici (EUR)</label><input id="ff_dnevnica_iznos" class="v6-inp v6-num" type="number" step="0.01" value="30" oninput="v6CalcTotal()" /></div>';
|
||
html += ' <div class="v6-fld"><label class="v6-lbl">💰 Ukupno dnevnice</label><input id="ff_dnevnice_uk" class="v6-inp v6-num v6-calc" type="number" step="0.01" readonly /></div>';
|
||
html += ' </div>';
|
||
html += ' <div class="v6-g3" style="margin-top:8px">';
|
||
html += ' <div class="v6-fld"><label class="v6-lbl">Predujam (EUR)</label><input id="ff_predujam" class="v6-inp v6-num" type="number" step="0.01" value="0" oninput="v6CalcTotal()" /></div>';
|
||
html += ' <div class="v6-fld v6-w2"><label class="v6-lbl">Napomena</label><textarea id="ff_napomena" class="v6-inp" rows="2"></textarea></div>';
|
||
html += ' </div>';
|
||
html += ' </div>';
|
||
|
||
// Totals footer
|
||
html += ' <div class="v6-tot">';
|
||
html += ' <div class="v6-tot-i"><div class="v6-lbl">Ukupni trošak</div><div class="v6-val" id="ff_uk_trosak">0,00 €</div></div>';
|
||
html += ' <div class="v6-tot-i"><div class="v6-lbl">Manje predujam</div><div class="v6-val" id="ff_minus_pred" style="color:#f0b429">−0,00 €</div></div>';
|
||
html += ' <div class="v6-tot-i"><div class="v6-lbl">Za isplatu</div><div class="v6-val" id="ff_za_isplatu">0,00 €</div></div>';
|
||
html += ' </div>';
|
||
} else {
|
||
// GENERIC FORM — auto-grid 2 columns
|
||
html += '<div class="v6-fs"><div class="v6-g2">';
|
||
let col = 0;
|
||
fields.forEach(f => {
|
||
const span = colSpan(f);
|
||
const req = f.required ? ' req' : '';
|
||
const reqAttr = f.required ? ' required' : '';
|
||
const fieldClass = span === 2 ? 'v6-fld v6-w2' : 'v6-fld';
|
||
html += '<div class="' + fieldClass + '">';
|
||
html += '<label class="v6-lbl' + req + '">' + (f.label||f.name) + '</label>';
|
||
if (f.type === 'textarea') html += '<textarea id="ff_' + f.name + '" class="v6-inp" rows="3"' + reqAttr + '></textarea>';
|
||
else if (f.type === 'select') html += '<select id="ff_' + f.name + '" class="v6-inp"' + reqAttr + '><option></option>' + (f.options||[]).map(o => '<option>' + o + '</option>').join('') + '</select>';
|
||
else if (f.type === 'checkbox') html += '<input id="ff_' + f.name + '" type="checkbox" />';
|
||
else {
|
||
const numClass = (f.type === 'number' || (f.name && (f.name.includes('iznos')||f.name.includes('km')||f.name.includes('amount')))) ? ' v6-num' : '';
|
||
html += '<input id="ff_' + f.name + '" class="v6-inp' + numClass + '" type="' + (f.type||'text') + '"' + reqAttr + ' />';
|
||
}
|
||
html += '</div>';
|
||
});
|
||
html += '</div></div>';
|
||
}
|
||
|
||
// Footer buttons
|
||
html += '<div class="v6-fs" style="display:flex;gap:8px;justify-content:flex-end">';
|
||
html += ' <button class="v6-btn" onclick="document.getElementById(\'formRender\').innerHTML=\'\'">Zatvori</button>';
|
||
html += ' <button class="v6-btn" onclick="submitForm(\'' + code + '\',\'draft\')">Spremi draft</button>';
|
||
html += ' <button class="v6-btn primary" onclick="submitForm(\'' + code + '\',\'submitted\')">Pošalji</button>';
|
||
html += '</div>';
|
||
html += '<div id="formStatus" style="padding:10px 16px;font-size:13px"></div>';
|
||
html += '</div>';
|
||
|
||
document.getElementById('formRender').innerHTML = html;
|
||
document.getElementById('formRender').scrollIntoView({behavior:'smooth'});
|
||
if (isPutni) v6InitPutni();
|
||
} catch(e) { alert(e.message); }
|
||
}
|
||
|
||
// V6 PUTNI NALOG HELPERS
|
||
window.v6Attachments = window.v6Attachments || [];
|
||
function v6InitPutni() {
|
||
v6Attachments = [];
|
||
v6CalcKM(); v6CalcCost(); v6CalcTotal();
|
||
}
|
||
async function v6GradAuto(input, fieldKey) {
|
||
const q = input.value.trim();
|
||
const ac = document.getElementById('ac_' + fieldKey);
|
||
if (!q || q.length < 2) { ac.classList.remove('show'); return; }
|
||
try {
|
||
const r = await fetch('/sport/api/ai/gradovi?q=' + encodeURIComponent(q) + '&limit=10');
|
||
const list = await r.json();
|
||
if (!list.length) { ac.classList.remove('show'); return; }
|
||
ac.innerHTML = list.map(g => '<div onclick="document.getElementById(\'ff_' + fieldKey + '\').value=\'' + g.replace(/\'/g,"\\'") + '\';this.parentNode.classList.remove(\'show\');v6CalcKM()">' + g + '</div>').join('');
|
||
ac.classList.add('show');
|
||
} catch (e) { ac.classList.remove('show'); }
|
||
}
|
||
async function v6CalcKM() {
|
||
const od = (document.getElementById('ff_polaziste')||{}).value;
|
||
const dod = (document.getElementById('ff_odrediste')||{}).value;
|
||
const aiKm = document.getElementById('ff_ai_km');
|
||
const total = document.getElementById('ff_km_total');
|
||
const kmPre = parseFloat((document.getElementById('ff_km_pre')||{}).value || 0);
|
||
const kmPost = parseFloat((document.getElementById('ff_km_post')||{}).value || 0);
|
||
|
||
// Manual override: km_post - km_pre
|
||
if (kmPre > 0 && kmPost > kmPre) {
|
||
if (total) total.value = (kmPost - kmPre).toFixed(1);
|
||
document.getElementById('aiHint').innerHTML = '✓ Računamo iz stanja brzinomjera: ' + kmPre + ' → ' + kmPost + ' = ' + (kmPost - kmPre) + ' km';
|
||
} else if (od && dod && od !== dod) {
|
||
try {
|
||
const r = await fetch('/sport/api/ai/distance?od=' + encodeURIComponent(od) + '&do=' + encodeURIComponent(dod));
|
||
const d = await r.json();
|
||
if (d.found) {
|
||
if (aiKm) aiKm.value = d.udaljenost_km;
|
||
if (total) total.value = (d.udaljenost_km * 2).toFixed(1);
|
||
document.getElementById('aiHint').innerHTML = '🤖 AI: ' + od + ' → ' + dod + ' = ' + d.udaljenost_km + ' km × 2 (povratak) = ' + (d.udaljenost_km * 2) + ' km · vrijeme: ~' + d.vrijeme_minute + ' min · izvor: ' + d.izvor;
|
||
} else {
|
||
document.getElementById('aiHint').innerHTML = '⚠️ ' + d.suggestion + ' Unesi ručno KM stanje brzinomjera ili upiši ukupno.';
|
||
}
|
||
} catch(e) { document.getElementById('aiHint').innerHTML = '⚠️ AI greška: ' + e.message; }
|
||
}
|
||
v6CalcCost();
|
||
}
|
||
function v6CalcCost() {
|
||
const km = parseFloat((document.getElementById('ff_km_total')||{}).value || 0);
|
||
const cij = parseFloat((document.getElementById('ff_cijena_km')||{}).value || 0.50);
|
||
const t = document.getElementById('ff_trosak_prijevoz');
|
||
if (t) t.value = (km * cij).toFixed(2);
|
||
v6CalcTotal();
|
||
}
|
||
function v6CalcTotal() {
|
||
const f = id => parseFloat((document.getElementById(id)||{}).value || 0);
|
||
const trprij = f('ff_trosak_prijevoz');
|
||
const cest = f('ff_cestarine'); const park = f('ff_parkirne');
|
||
const gor = f('ff_gorivo'); const smj = f('ff_smjestaj'); const ost = f('ff_ostali');
|
||
const dn_n = f('ff_dnevnice_n'); const dn_iz = f('ff_dnevnica_iznos');
|
||
const pred = f('ff_predujam');
|
||
const dn_uk = dn_n * dn_iz;
|
||
const dn_uk_el = document.getElementById('ff_dnevnice_uk');
|
||
if (dn_uk_el) dn_uk_el.value = dn_uk.toFixed(2);
|
||
const ukupno = trprij + cest + park + gor + smj + ost + dn_uk;
|
||
const za_isp = ukupno - pred;
|
||
const fmt = n => n.toLocaleString('hr-HR', {minimumFractionDigits:2,maximumFractionDigits:2}) + ' €';
|
||
const uk_el = document.getElementById('ff_uk_trosak');
|
||
const mp_el = document.getElementById('ff_minus_pred');
|
||
const zi_el = document.getElementById('ff_za_isplatu');
|
||
if (uk_el) uk_el.textContent = fmt(ukupno);
|
||
if (mp_el) mp_el.textContent = '−' + fmt(pred);
|
||
if (zi_el) zi_el.textContent = fmt(za_isp);
|
||
}
|
||
async function v6UploadPrilog(input) {
|
||
const files = input.files;
|
||
if (!files || !files.length) return;
|
||
const list = document.getElementById('v6AttList');
|
||
for (const file of files) {
|
||
const item = document.createElement('div');
|
||
item.className = 'v6-att-i';
|
||
item.innerHTML = '<span>📄 ' + file.name + '</span><span class="v6-tag">OCR...</span>';
|
||
list.appendChild(item);
|
||
const fd = new FormData();
|
||
fd.append('file', file);
|
||
fd.append('tip', 'racun');
|
||
try {
|
||
const r = await fetch('/sport/api/ai/ocr-prilog', {method:'POST', body: fd});
|
||
const d = await r.json();
|
||
const tag = d.ai_amount ? d.tip : 'parsed';
|
||
const amt = d.ai_amount ? (d.ai_amount.toFixed(2) + ' €') : '?';
|
||
const vendor = d.ai_vendor ? d.ai_vendor.slice(0, 30) : '';
|
||
item.innerHTML = '<span>📄 ' + file.name + '</span><span class="v6-tag">✓ ' + tag + '</span><span style="color:#788798;font-size:11px">' + vendor + '</span><span class="v6-amt">' + amt + '</span>'
|
||
+ '<select onchange="v6PrilogAssign(' + (v6Attachments.length) + ',this.value)" class="v6-inp" style="margin-left:auto;width:auto;font-size:11px">'
|
||
+ '<option value="">— pridruži —</option>'
|
||
+ '<option value="ff_cestarine">Cestarine</option>'
|
||
+ '<option value="ff_parkirne">Parkirne</option>'
|
||
+ '<option value="ff_gorivo">Gorivo</option>'
|
||
+ '<option value="ff_smjestaj">Smještaj</option>'
|
||
+ '<option value="ff_ostali">Ostali</option>'
|
||
+ '</select>';
|
||
v6Attachments.push({file: file.name, ocr: d, assigned_to: null});
|
||
} catch(e) {
|
||
item.innerHTML = '<span>📄 ' + file.name + '</span><span class="v6-tag" style="background:#6e2a2a">✗ greška</span>';
|
||
}
|
||
}
|
||
input.value = '';
|
||
}
|
||
function v6PrilogAssign(idx, fieldId) {
|
||
const att = v6Attachments[idx];
|
||
if (!att || !fieldId) return;
|
||
att.assigned_to = fieldId;
|
||
const el = document.getElementById(fieldId);
|
||
if (el && att.ocr.ai_amount) {
|
||
const cur = parseFloat(el.value || 0);
|
||
el.value = (cur + att.ocr.ai_amount).toFixed(2);
|
||
v6CalcTotal();
|
||
}
|
||
}
|
||
|
||
async function submitForm(code, status) {
|
||
const tpl = await v2Fetch('/forms/templates/'+code);
|
||
let fields = (tpl.schema_json && tpl.schema_json.fields) || [];
|
||
const data = {};
|
||
|
||
// Always read all standard fields by ID
|
||
fields.forEach(f => {
|
||
const el = document.getElementById('ff_'+f.name);
|
||
if (!el) return;
|
||
data[f.name] = f.type==='checkbox' ? el.checked : el.value;
|
||
});
|
||
|
||
// V6 putni nalog extra fields
|
||
const isPutni = code === 'obracun_putnih_troskova' || code === 'putni_nalog';
|
||
if (isPutni) {
|
||
['polaziste','odrediste','ai_km','datum_polaska','datum_povratka',
|
||
'km_pre','km_post','km_total','cijena_km','trosak_prijevoz',
|
||
'cestarine','parkirne','gorivo','smjestaj','ostali',
|
||
'dnevnice_n','dnevnica_iznos','dnevnice_uk','predujam','napomena'].forEach(k => {
|
||
const el = document.getElementById('ff_'+k);
|
||
if (el) data[k] = el.value;
|
||
});
|
||
// Attach OCR data
|
||
if (window.v6Attachments && v6Attachments.length) {
|
||
data._attachments = v6Attachments.map(a => ({
|
||
file: a.file, ocr: {amount: a.ocr.ai_amount, date: a.ocr.ai_date, vendor: a.ocr.ai_vendor, oib: a.ocr.ai_oib},
|
||
assigned_to: a.assigned_to
|
||
}));
|
||
}
|
||
}
|
||
|
||
try {
|
||
const d = await v2Fetch('/forms/submit', {method:'POST', body: JSON.stringify({template_code: code, data, status})});
|
||
document.getElementById('formStatus').innerHTML = '✅ Spremljeno · #'+d.submission_id+' · '+(d.reference_no||'');
|
||
} catch(e) { document.getElementById('formStatus').innerHTML = '❌ '+e.message; }
|
||
}
|
||
|
||
async function pageUsers() {
|
||
// GATE — moraš biti prijavljen
|
||
if (!state.v2Token || !state.v2User) {
|
||
document.getElementById('content').innerHTML = `
|
||
<div class="page-h"><h2>🔐 Korisnici i prava</h2><p class="muted">Za pristup ovoj stranici potrebna je prijava.</p></div>
|
||
<div class="card" style="max-width:480px;margin-top:18px;padding:24px;text-align:center">
|
||
<div style="font-size:48px;margin-bottom:8px">🔐</div>
|
||
<h3 style="margin-bottom:6px">Prijavi se za nastavak</h3>
|
||
<p class="muted" style="margin-bottom:16px">Modul "Korisnici" je dostupan samo prijavljenim administratorima.</p>
|
||
<button class="btn" onclick="showLogin('user')" style="min-width:160px">Prijavi se</button>
|
||
</div>`;
|
||
return;
|
||
}
|
||
|
||
setTopbar('Administracija', 'Korisnici i prava');
|
||
const me = state.v2User || {};
|
||
const isAdmin = ['super_admin','pgz_admin'].includes(me.user_type);
|
||
const isSuper = me.user_type === 'super_admin';
|
||
|
||
document.getElementById('content').innerHTML = `
|
||
<div class="page-h" style="margin-bottom:14px">
|
||
<h2>👥 Korisnici i prava</h2>
|
||
<p class="muted">Multi-tenant · ${isSuper?'super_admin':isAdmin?'pgz_admin':'pregled'} · klik na red → akcije</p>
|
||
</div>
|
||
|
||
${isAdmin ? `
|
||
<div class="card" style="margin-bottom:14px;padding:14px">
|
||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
||
<input id="usrSearch" class="inp" placeholder="🔍 traži email / ime / prezime" style="flex:1;min-width:240px" oninput="usrSearch()">
|
||
<select id="usrFilterType" class="inp" onchange="usrSearch()">
|
||
<option value="">Svi tipovi</option>
|
||
<option value="super_admin">super_admin</option>
|
||
<option value="pgz_admin">pgz_admin</option>
|
||
<option value="pgz_user">pgz_user</option>
|
||
<option value="pgz_finance">pgz_finance</option>
|
||
<option value="pgz_zzjz">pgz_zzjz</option>
|
||
<option value="savez_admin">savez_admin</option>
|
||
<option value="klub_admin">klub_admin</option>
|
||
<option value="klub_user">klub_user</option>
|
||
<option value="klub_clan">klub_clan</option>
|
||
</select>
|
||
<button class="btn ri-btn-primary" onclick="usrShowCreate()">+ Novi korisnik</button>
|
||
<button class="btn" onclick="usrShowAudit()">📋 Audit</button>
|
||
</div>
|
||
</div>` : ''}
|
||
|
||
<div class="card" style="overflow:auto">
|
||
<div id="usrList">Učitavam...</div>
|
||
</div>
|
||
|
||
<div class="modal-bg" id="usrModal" style="display:none">
|
||
<div class="modal" id="usrModalBody" style="min-width:420px;max-width:560px"></div>
|
||
</div>
|
||
`;
|
||
|
||
await usrLoadList();
|
||
}
|
||
|
||
let _usrSearchTimer = null;
|
||
function usrSearch() {
|
||
clearTimeout(_usrSearchTimer);
|
||
_usrSearchTimer = setTimeout(usrLoadList, 300);
|
||
}
|
||
|
||
async function usrLoadList() {
|
||
const q = (document.getElementById('usrSearch')||{}).value || '';
|
||
const ut = (document.getElementById('usrFilterType')||{}).value || '';
|
||
const params = new URLSearchParams();
|
||
if (q) params.set('q', q);
|
||
if (ut) params.set('user_type', ut);
|
||
params.set('limit', '100');
|
||
|
||
try {
|
||
const d = await api('/api/v2/users/list?' + params.toString());
|
||
const me = state.v2User || {};
|
||
const isAdmin = ['super_admin','pgz_admin'].includes(me.user_type);
|
||
const isSuper = me.user_type === 'super_admin';
|
||
|
||
if (!d.results || d.results.length === 0) {
|
||
document.getElementById('usrList').innerHTML = '<div class="muted" style="padding:24px;text-align:center">Nema korisnika u tvom dosegu.</div>';
|
||
return;
|
||
}
|
||
|
||
let html = `<div class="muted" style="font-size:11px;margin-bottom:8px">${d.count} / ${d.total} korisnika</div>`;
|
||
html += '<table class="ri-tbl ri-sortable"><thead><tr><th>#</th><th>Email</th><th>Ime i prezime</th><th>Tip</th><th>Klub/Savez</th><th>Status</th><th>Lock</th><th>Last login</th><th style="text-align:right">Akcije</th></tr></thead><tbody>';
|
||
for (const u of d.results) {
|
||
const locked = u.locked_until && new Date(u.locked_until) > new Date();
|
||
const failBadge = u.failed_login_count > 0 ? `<span class="risk-medium">${u.failed_login_count}</span>` : '';
|
||
const lockBadge = locked ? '<span class="risk-critical">LOCKED</span>' : (u.aktivan ? '<span class="risk-low">aktivan</span>' : '<span class="risk-high">isključen</span>');
|
||
const mustCh = u.must_change_pwd ? ' <span class="risk-medium" title="Mora promijeniti lozinku">!</span>' : '';
|
||
const llogin = u.last_login ? new Date(u.last_login).toLocaleDateString('hr-HR') : '<span class="muted">nikad</span>';
|
||
const klubLabel = u.klub_id ? `klub#${u.klub_id}` : (u.savez_id ? `savez#${u.savez_id}` : '-');
|
||
let actions = '';
|
||
if (isAdmin && u.id !== me.id) {
|
||
actions = `
|
||
<button class="btn ri-btn-ghost" onclick="usrShowEdit(${u.id})" style="padding:2px 6px;font-size:10px">edit</button>
|
||
<button class="btn ri-btn-ghost" onclick="usrResetPwd(${u.id}, '${(u.email||'').replace(/'/g,'')}')" style="padding:2px 6px;font-size:10px">reset pwd</button>
|
||
<button class="btn ri-btn-ghost" onclick="usrToggle(${u.id})" style="padding:2px 6px;font-size:10px">${u.aktivan?'isključi':'aktiviraj'}</button>
|
||
${locked ? `<button class="btn ri-btn-ghost" onclick="usrUnlock(${u.id})" style="padding:2px 6px;font-size:10px">unlock</button>` : ''}
|
||
${isSuper ? `<button class="btn ri-btn-ghost" onclick="usrImpersonate(${u.id})" style="padding:2px 6px;font-size:10px;color:var(--amber)">impersonate</button>` : ''}
|
||
<button class="btn ri-btn-ghost" onclick="usrShowAudit(${u.id})" style="padding:2px 6px;font-size:10px">audit</button>`;
|
||
} else if (u.id === me.id) {
|
||
actions = `<button class="btn ri-btn-ghost" onclick="usrShowEdit(${u.id})" style="padding:2px 6px;font-size:10px">edit (ja)</button>`;
|
||
}
|
||
html += `<tr>
|
||
<td class="mono">${u.id}</td>
|
||
<td><b>${u.email}</b>${mustCh}</td>
|
||
<td>${(u.ime||'')+' '+(u.prezime||'')}</td>
|
||
<td><span class="mono" style="font-size:10px;color:var(--text3)">${u.user_type||'?'}</span></td>
|
||
<td class="mono" style="font-size:10px">${klubLabel}</td>
|
||
<td>${lockBadge} ${failBadge}</td>
|
||
<td>${locked ? `<span class="muted" title="${u.locked_until}">do ${new Date(u.locked_until).toLocaleTimeString('hr-HR',{hour:'2-digit',minute:'2-digit'})}</span>` : '-'}</td>
|
||
<td>${llogin}</td>
|
||
<td style="text-align:right;white-space:nowrap">${actions}</td>
|
||
</tr>`;
|
||
}
|
||
html += '</tbody></table>';
|
||
document.getElementById('usrList').innerHTML = html;
|
||
} catch(e) {
|
||
document.getElementById('usrList').innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>';
|
||
}
|
||
}
|
||
|
||
function usrModal(html) {
|
||
const m = document.getElementById('usrModal');
|
||
const b = document.getElementById('usrModalBody');
|
||
if (!m || !b) return;
|
||
b.innerHTML = html;
|
||
m.style.display = 'flex';
|
||
}
|
||
function usrModalClose() {
|
||
const m = document.getElementById('usrModal');
|
||
if (m) m.style.display = 'none';
|
||
}
|
||
|
||
function usrShowCreate() {
|
||
usrModal(`
|
||
<h3 style="margin-bottom:14px">+ Novi korisnik</h3>
|
||
<input id="ncEmail" class="inp" placeholder="email@pgz.hr" style="width:100%;margin-bottom:8px">
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">
|
||
<input id="ncIme" class="inp" placeholder="Ime">
|
||
<input id="ncPrezime" class="inp" placeholder="Prezime">
|
||
</div>
|
||
<select id="ncType" class="inp" style="width:100%;margin-bottom:8px">
|
||
<option value="klub_user">klub_user</option>
|
||
<option value="klub_admin">klub_admin</option>
|
||
<option value="klub_clan">klub_clan</option>
|
||
<option value="savez_user">savez_user</option>
|
||
<option value="savez_admin">savez_admin</option>
|
||
<option value="pgz_user">pgz_user</option>
|
||
<option value="pgz_finance">pgz_finance</option>
|
||
<option value="pgz_zzjz">pgz_zzjz</option>
|
||
<option value="pgz_admin">pgz_admin</option>
|
||
</select>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">
|
||
<input id="ncKlub" class="inp" type="number" placeholder="klub_id">
|
||
<input id="ncSavez" class="inp" type="number" placeholder="savez_id">
|
||
</div>
|
||
<input id="ncTel" class="inp" placeholder="Telefon" style="width:100%;margin-bottom:8px">
|
||
<p class="muted" style="font-size:10px;margin-bottom:12px">Default lozinka: <code>PgzSport2026!</code> + must_change_pwd=true</p>
|
||
<div id="ncErr" class="banner crit" style="display:none;margin-bottom:8px"></div>
|
||
<div style="display:flex;gap:8px">
|
||
<button class="btn ri-btn-primary" style="flex:1" onclick="usrCreate()">Kreiraj</button>
|
||
<button class="btn" onclick="usrModalClose()">Odustani</button>
|
||
</div>
|
||
`);
|
||
}
|
||
|
||
async function usrCreate() {
|
||
const body = {
|
||
email: document.getElementById('ncEmail').value.trim(),
|
||
ime: document.getElementById('ncIme').value.trim() || null,
|
||
prezime: document.getElementById('ncPrezime').value.trim() || null,
|
||
user_type: document.getElementById('ncType').value,
|
||
klub_id: parseInt(document.getElementById('ncKlub').value) || null,
|
||
savez_id: parseInt(document.getElementById('ncSavez').value) || null,
|
||
telefon: document.getElementById('ncTel').value.trim() || null,
|
||
};
|
||
const err = document.getElementById('ncErr');
|
||
err.style.display = 'none';
|
||
if (!body.email) { err.textContent = 'Email je obavezan'; err.style.display='block'; return; }
|
||
try {
|
||
const d = await api('/api/v2/users/create', { method:'POST', body: JSON.stringify(body) });
|
||
alert('Kreiran user #' + d.id + (d.temporary_password ? '\nPrivremena lozinka: ' + d.temporary_password : ''));
|
||
usrModalClose();
|
||
await usrLoadList();
|
||
} catch(e) { err.textContent = e.message; err.style.display='block'; }
|
||
}
|
||
|
||
async function usrShowEdit(uid) {
|
||
try {
|
||
const d = await api('/api/v2/users/list?limit=200');
|
||
const u = (d.results || []).find(x => x.id === uid);
|
||
if (!u) { alert('Korisnik nije u tvom dosegu'); return; }
|
||
usrModal(`
|
||
<h3 style="margin-bottom:14px">✎ Edit #${u.id} · ${u.email}</h3>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">
|
||
<input id="eIme" class="inp" placeholder="Ime" value="${u.ime||''}">
|
||
<input id="ePrezime" class="inp" placeholder="Prezime" value="${u.prezime||''}">
|
||
</div>
|
||
<select id="eType" class="inp" style="width:100%;margin-bottom:8px">
|
||
${['super_admin','pgz_admin','pgz_user','pgz_finance','pgz_zzjz','savez_admin','savez_user','klub_admin','klub_user','klub_clan'].map(t=>`<option value="${t}" ${u.user_type===t?'selected':''}>${t}</option>`).join('')}
|
||
</select>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">
|
||
<input id="eKlub" class="inp" type="number" placeholder="klub_id" value="${u.klub_id||''}">
|
||
<input id="eSavez" class="inp" type="number" placeholder="savez_id" value="${u.savez_id||''}">
|
||
</div>
|
||
<input id="eTel" class="inp" placeholder="Telefon" value="${u.telefon||''}" style="width:100%;margin-bottom:8px">
|
||
<input id="eOib" class="inp" placeholder="OIB" value="${u.oib||''}" style="width:100%;margin-bottom:12px">
|
||
<div id="eErr" class="banner crit" style="display:none;margin-bottom:8px"></div>
|
||
<div style="display:flex;gap:8px">
|
||
<button class="btn ri-btn-primary" style="flex:1" onclick="usrEdit(${u.id})">Spremi</button>
|
||
<button class="btn" onclick="usrModalClose()">Odustani</button>
|
||
</div>
|
||
`);
|
||
} catch(e) { alert('Greška: '+e.message); }
|
||
}
|
||
|
||
async function usrEdit(uid) {
|
||
const body = {
|
||
ime: document.getElementById('eIme').value.trim() || null,
|
||
prezime: document.getElementById('ePrezime').value.trim() || null,
|
||
user_type: document.getElementById('eType').value,
|
||
klub_id: parseInt(document.getElementById('eKlub').value) || null,
|
||
savez_id: parseInt(document.getElementById('eSavez').value) || null,
|
||
telefon: document.getElementById('eTel').value.trim() || null,
|
||
oib: document.getElementById('eOib').value.trim() || null,
|
||
};
|
||
try {
|
||
await api('/api/v2/users/' + uid, { method:'PUT', body: JSON.stringify(body) });
|
||
usrModalClose();
|
||
await usrLoadList();
|
||
} catch(e) {
|
||
document.getElementById('eErr').textContent = e.message;
|
||
document.getElementById('eErr').style.display = 'block';
|
||
}
|
||
}
|
||
|
||
async function usrResetPwd(uid, email) {
|
||
if (!confirm('Resetirati lozinku korisnika ' + email + '?\nGenerirat će se nova privremena lozinka.')) return;
|
||
try {
|
||
const d = await api('/api/v2/users/' + uid + '/reset-password', { method:'POST' });
|
||
prompt('Nova privremena lozinka — kopiraj i pošalji korisniku:', d.temporary_password);
|
||
await usrLoadList();
|
||
} catch(e) { alert('Greška: ' + e.message); }
|
||
}
|
||
|
||
async function usrToggle(uid) {
|
||
try {
|
||
await api('/api/v2/users/' + uid + '/toggle-active', { method:'POST' });
|
||
await usrLoadList();
|
||
} catch(e) { alert('Greška: ' + e.message); }
|
||
}
|
||
|
||
async function usrUnlock(uid) {
|
||
try {
|
||
await api('/api/v2/users/' + uid + '/unlock', { method:'POST' });
|
||
await usrLoadList();
|
||
} catch(e) { alert('Greška: ' + e.message); }
|
||
}
|
||
|
||
async function usrImpersonate(uid) {
|
||
if (!confirm('Impersonate korisnika #' + uid + '?\nIzdaje se 2h token i ti gubiš svoju sesiju u ovom tabu.')) return;
|
||
try {
|
||
const d = await api('/api/v2/admin/impersonate', { method:'POST', body: JSON.stringify({target_user_id: uid}) });
|
||
localStorage.setItem('rinet_v2_token', d.token);
|
||
localStorage.setItem('rinet_v2_user', JSON.stringify(d.as_user));
|
||
alert('Sad si u ulozi: ' + d.as_user.email + '\nDo ' + new Date(d.expires_at).toLocaleString('hr-HR'));
|
||
await checkRole();
|
||
render();
|
||
} catch(e) { alert('Greška: ' + e.message); }
|
||
}
|
||
|
||
async function usrShowAudit(uid) {
|
||
try {
|
||
const url = uid ? '/api/v2/users/' + uid + '/audit?limit=100' : '/api/v2/admin/audit?limit=100';
|
||
const d = await api(url);
|
||
let html = '<h3 style="margin-bottom:10px">📋 Audit ' + (uid?`za #${uid}`:'(globalno)') + '</h3>';
|
||
if (!d.results.length) {
|
||
html += '<div class="muted" style="padding:14px">Nema zapisa.</div>';
|
||
} else {
|
||
html += '<table class="ri-tbl ri-sortable"><thead><tr><th>Datum</th><th>User</th><th>Akcija</th></tr></thead><tbody>';
|
||
for (const a of d.results) {
|
||
const dt = new Date(a.created_at).toLocaleString('hr-HR');
|
||
const who = a.email ? a.email : ('user#'+a.user_id);
|
||
html += `<tr><td class="mono" style="font-size:10px">${dt}</td><td>${who}</td><td class="mono" style="font-size:10px">${a.action||''}</td></tr>`;
|
||
}
|
||
html += '</tbody></table>';
|
||
}
|
||
html += '<div style="text-align:right;margin-top:14px"><button class="btn" onclick="usrModalClose()">Zatvori</button></div>';
|
||
usrModal(html);
|
||
} catch(e) { alert('Greška: ' + e.message); }
|
||
}
|
||
async function createUser() {
|
||
const body = {
|
||
email: document.getElementById('nuEmail').value,
|
||
full_name: document.getElementById('nuName').value,
|
||
password: document.getElementById('nuPwd').value,
|
||
role: document.getElementById('nuRole').value,
|
||
klub_id: parseInt(document.getElementById('nuKlub').value||'0')||null
|
||
};
|
||
try {
|
||
const d = await v2Fetch('/users', {method:'POST', body: JSON.stringify(body)});
|
||
document.getElementById('usrStatus').innerHTML = `✅ User #${d.user_id} kreiran`;
|
||
setTimeout(pageUsers, 800);
|
||
} catch(e) { document.getElementById('usrStatus').innerHTML = '❌ '+e.message; }
|
||
}
|
||
|
||
// V2 LOGIN BRIDGE — overlay on existing showLogin/doLogin if it uses old endpoint
|
||
window.v2Login = async function(email, pwd) {
|
||
const r = await fetch('/sport/api/v2/auth/login', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({email, password: pwd})});
|
||
const d = await r.json();
|
||
if (!r.ok) throw new Error(d.detail||'Login failed');
|
||
localStorage.setItem('rinet_v2_token', d.token);
|
||
localStorage.setItem('rinet_v2_user', JSON.stringify(d.user));
|
||
return d;
|
||
};
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// KATEGORIJE auto-recalculate when sport/datum changes
|
||
// ═══════════════════════════════════════════════════════
|
||
async function spRecalcCats() {
|
||
const sport = (document.getElementById('spSport')||{}).value;
|
||
const dob = (document.getElementById('spDob')||{}).value;
|
||
const preview = document.getElementById('spKatPreview');
|
||
if (!preview) return;
|
||
if (!sport || !dob) {
|
||
preview.style.display = 'none';
|
||
return;
|
||
}
|
||
preview.style.display = 'block';
|
||
preview.innerHTML = '<div class="muted">Računam kategoriju…</div>';
|
||
try {
|
||
const r = await fetch('/sport/api/v2/dobne-kategorije/auto-assign', {
|
||
method:'POST', headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify({datum_rodenja: dob, sport: sport})
|
||
});
|
||
const d = await r.json();
|
||
if (d.warning) {
|
||
preview.innerHTML = `<div class="muted">⚠️ ${d.warning}</div>`;
|
||
return;
|
||
}
|
||
let html = `<div style="font-size:11px;color:var(--text2);margin-bottom:6px">DOB: <b>${d.starost} godina</b> · referentna sezona ${d.referentna_godina}</div>`;
|
||
if (d.primary) {
|
||
html += `<div style="margin-bottom:6px">
|
||
<span class="muted" style="font-size:10px">Glavna kategorija:</span>
|
||
<span style="display:inline-block;padding:3px 10px;background:var(--accent);color:white;border-radius:4px;font-weight:600;margin-left:6px">
|
||
${d.primary.oznaka} · ${d.primary.naziv}
|
||
</span>
|
||
<span class="muted" style="font-size:10px;margin-left:6px">(${d.primary.organizacija})</span>
|
||
</div>`;
|
||
}
|
||
if (d.additional && d.additional.length) {
|
||
html += `<div style="margin-bottom:6px"><span class="muted" style="font-size:10px">+ Pripadne:</span> ${
|
||
d.additional.map(k => `<span style="display:inline-block;padding:2px 8px;background:var(--bg4);border:1px solid var(--border2);border-radius:4px;margin:0 4px;font-size:11px">${k.oznaka}</span>`).join('')
|
||
}</div>`;
|
||
}
|
||
if (d.promocije && d.promocije.length) {
|
||
html += `<div style="margin-top:8px;padding-top:8px;border-top:1px solid var(--border)">
|
||
<div class="muted" style="font-size:11px;margin-bottom:6px">📈 Mogućnost promocije u stariju selekciju (mladi se često priključuju glavnoj ekipi):</div>`;
|
||
d.promocije.forEach(k => {
|
||
html += `<label style="display:block;margin:3px 0;cursor:pointer;font-size:12px">
|
||
<input type="checkbox" name="spPromo" value="${k.oznaka}" style="margin-right:6px">
|
||
<b>${k.oznaka}</b> · ${k.naziv} <span class="muted" style="font-size:10px">(${k.min_godina||'?'}-${k.max_godina||'∞'} god, ${k.organizacija})</span>
|
||
</label>`;
|
||
});
|
||
html += '</div>';
|
||
}
|
||
preview.innerHTML = html;
|
||
} catch(e) {
|
||
preview.innerHTML = `<div class="banner crit">Greška: ${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// KATEGORIJE PAGE — pregled i statistika po sportu
|
||
// ═══════════════════════════════════════════════════════
|
||
async function pageKategorije() {
|
||
const c = document.getElementById('content');
|
||
setTopbar('Dobne kategorije', 'Pravila iz HR sportskih saveza · auto-asssign po dobi');
|
||
c.innerHTML = '<div class="card" style="padding:20px">Učitavam…</div>';
|
||
try {
|
||
const d = await fetch('/sport/api/v2/dobne-kategorije/by-sport').then(r=>r.json());
|
||
let html = `<div class="card" style="padding:14px;margin-bottom:14px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
||
<div>
|
||
<h3 style="margin:0">Dobne kategorije po sportu</h3>
|
||
<div class="muted" style="font-size:11px;margin-top:4px">${d.count} sportova s definiranim kategorijama prema pravilima HR saveza</div>
|
||
</div>
|
||
<button class="btn" onclick="recalcAllCategories()">↻ Recalc svih sportaša</button>
|
||
</div>
|
||
</div>`;
|
||
|
||
for (const sp of d.results) {
|
||
html += `<div class="card" style="margin-bottom:10px">
|
||
<div style="padding:10px 14px;background:var(--bg3);border-bottom:1px solid var(--border);display:flex;justify-content:space-between">
|
||
<h4 style="margin:0;text-transform:capitalize">${sp.sport}</h4>
|
||
<span class="muted" style="font-size:11px">${sp.broj} kategorija</span>
|
||
</div>
|
||
<table class="ri-tbl ri-sortable" style="margin:0">
|
||
<thead><tr><th>Oznaka</th><th>Naziv</th><th>Dob</th><th>Organizacija</th><th>Napomena</th><th>Promocija</th></tr></thead>
|
||
<tbody>`;
|
||
for (const k of sp.kategorije) {
|
||
const dobRange = `${k.min_godina ?? 0}-${k.max_godina ?? '∞'}`;
|
||
const promo = k.promocija_dozvoljena ? '✓' : '—';
|
||
html += `<tr>
|
||
<td><span style="display:inline-block;padding:2px 8px;background:var(--accent);color:white;border-radius:4px;font-weight:600;font-size:11px">${k.oznaka||'-'}</span></td>
|
||
<td><b>${k.naziv}</b></td>
|
||
<td class="mono">${dobRange}</td>
|
||
<td class="muted">${k.organizacija||'-'}</td>
|
||
<td class="muted" style="font-size:10px">${k.napomena||''}</td>
|
||
<td style="text-align:center">${promo}</td>
|
||
</tr>`;
|
||
}
|
||
html += '</tbody></table></div>';
|
||
}
|
||
c.innerHTML = html;
|
||
} catch(e) { c.innerHTML = '<div class="banner crit">Greška: '+e.message+'</div>'; }
|
||
}
|
||
|
||
async function recalcAllCategories() {
|
||
if (!confirm('Pokrenuti recalc kategorija za sve sportaše? (može trajati ~20s)')) return;
|
||
try {
|
||
const r = await api('/api/v2/sportas/recalc-all-categories', { method:'POST' });
|
||
alert(`Updated: ${r.updated}, skipped: ${r.skipped_no_sport}, errors: ${r.errors}`);
|
||
} catch(e) { alert('Greška: ' + e.message); }
|
||
}
|
||
|
||
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// DOKUMENTI / PRAVILNICI / ZAKONI — RAG + AI Legal Expert
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
let _dokState = { filter_razina:'', filter_vrsta:'', filter_organizacija:'', filter_sport:'', q:'' };
|
||
|
||
async function pageDokumenti() {
|
||
const c = document.getElementById('content');
|
||
setTopbar('Pravilnici i zakoni', 'Baza znanja zakona, pravilnika, statuta i programa za sport u PGŽ · AI legal expert');
|
||
|
||
c.innerHTML = `
|
||
<div class="card" style="padding:14px;margin-bottom:14px">
|
||
<h3 style="margin:0 0 10px 0">🤖 Hybrid AI Agent <span class="muted" style="font-size:11px;font-weight:400">— SQL (operativni podaci) + RAG (zakoni i pravilnici)</span></h3>
|
||
<div style="display:flex;gap:8px;margin-bottom:10px">
|
||
<input id="dokAskQ" class="inp" style="flex:1" placeholder="Npr.: Tko je trener HNK Rijeke? · Sportski objekti u Crikvenici? · Koje obveze ima klub po Zakonu o sportu?" />
|
||
<button class="btn" onclick="dokAsk()">🤖 Pitaj</button>
|
||
</div>
|
||
<div id="dokAskResult"></div>
|
||
</div>
|
||
|
||
<div class="card" style="padding:14px;margin-bottom:14px">
|
||
<h4 style="margin:0 0 10px 0">📚 Pretraga dokumenata (RAG vector search)</h4>
|
||
<div style="display:grid;grid-template-columns:2fr 1fr 1fr 1fr 1fr;gap:8px;margin-bottom:8px">
|
||
<input id="dokQ" class="inp" placeholder="Pretraga po sadržaju ili nazivu..." onkeydown="if(event.key==='Enter')dokSearch()" />
|
||
<select id="dokRazina" class="inp" onchange="dokLoadList()">
|
||
<option value="">— sve razine —</option>
|
||
<option>RH</option><option>EU</option><option>HOO</option>
|
||
<option>Savez</option><option>PGZ</option><option>Grad Rijeka</option>
|
||
</select>
|
||
<select id="dokVrsta" class="inp" onchange="dokLoadList()">
|
||
<option value="">— sve vrste —</option>
|
||
<option>zakon</option><option>pravilnik</option><option>pravilnik_savez</option>
|
||
<option>statut</option><option>strategija</option><option>program</option>
|
||
<option>plan</option><option>odluka</option><option>raspodjela</option>
|
||
<option>izvjestaj</option><option>natjecaj</option>
|
||
</select>
|
||
<select id="dokSport" class="inp" onchange="dokLoadList()">
|
||
<option value="">— svi sportovi —</option>
|
||
<option>nogomet</option><option>rukomet</option><option>košarka</option>
|
||
<option>odbojka</option><option>vaterpolo</option><option>plivanje</option>
|
||
<option>boćanje</option><option>tenis</option><option>stolni tenis</option>
|
||
<option>atletika</option><option>veslanje</option><option>jedriličarstvo</option>
|
||
<option>karate</option><option>judo</option><option>taekwondo</option>
|
||
<option>biciklizam</option><option>šah</option><option>lov</option>
|
||
</select>
|
||
<button class="btn" onclick="dokSearch()">🔎 Search</button>
|
||
</div>
|
||
<div id="dokSearchResult" style="margin-top:10px"></div>
|
||
</div>
|
||
|
||
<div id="dokListBox" class="card" style="padding:14px"></div>`;
|
||
|
||
dokLoadList();
|
||
}
|
||
|
||
async function dokLoadList() {
|
||
const r = document.getElementById('dokRazina').value;
|
||
const v = document.getElementById('dokVrsta').value;
|
||
const sp = document.getElementById('dokSport').value;
|
||
let url = '/sport/api/v2/dokumenti/list?limit=300';
|
||
if (r) url += '&razina=' + encodeURIComponent(r);
|
||
if (v) url += '&vrsta=' + encodeURIComponent(v);
|
||
if (sp) url += '&sport=' + encodeURIComponent(sp);
|
||
const box = document.getElementById('dokListBox');
|
||
box.innerHTML = '<div class="muted">Učitavam…</div>';
|
||
try {
|
||
const d = await fetch(url).then(x => x.json());
|
||
let html = `<div style="display:flex;justify-content:space-between;margin-bottom:10px">
|
||
<div><b>${d.count}</b> dokumenata</div>
|
||
<div class="muted" style="font-size:11px">${[r,v,sp].filter(Boolean).join(' · ') || 'svi filteri'}</div>
|
||
</div>`;
|
||
if (!d.results.length) {
|
||
html += '<div class="muted">Nema rezultata.</div>';
|
||
} else {
|
||
html += '<table class="ri-tbl ri-sortable" style="margin:0"><thead><tr><th>Razina</th><th>Vrsta</th><th>Naziv</th><th>Organizacija</th><th>Sport</th><th>Glasnik</th><th></th></tr></thead><tbody>';
|
||
for (const doc of d.results) {
|
||
const razinaCss = {
|
||
'RH':'background:#0066cc;color:white',
|
||
'EU':'background:#003399;color:white',
|
||
'HOO':'background:#fb923c;color:white',
|
||
'Savez':'background:#7c3aed;color:white',
|
||
'PGZ':'background:#10b981;color:white',
|
||
'Grad Rijeka':'background:#dc2626;color:white'
|
||
}[doc.razina] || 'background:var(--bg4);color:var(--text)';
|
||
html += `<tr>
|
||
<td><span style="display:inline-block;padding:2px 6px;border-radius:3px;font-size:10px;font-weight:600;${razinaCss}">${doc.razina||'-'}</span></td>
|
||
<td><span class="muted" style="font-size:10px">${doc.vrsta||'-'}</span></td>
|
||
<td><b style="cursor:pointer;color:var(--accent)" onclick="dokView(${doc.id})">${doc.naziv}</b>
|
||
<div class="muted" style="font-size:10px">${doc.kratak_opis||''}</div></td>
|
||
<td class="muted" style="font-size:11px">${doc.organizacija||'-'}</td>
|
||
<td class="muted" style="font-size:11px">${doc.sport||'-'}</td>
|
||
<td class="muted" style="font-size:10px">${doc.sluzbeni_glasnik||'-'}</td>
|
||
<td><div class="ri-actions">
|
||
<button class="ri-icon-btn-sm" onclick="dokView(${doc.id})" title="Pregled detalja">${iconEye()}</button>
|
||
<button class="ri-icon-btn-sm" onclick="window.open('/sport/api/v2/dokumenti/${doc.id}/pdf','_blank')" title="Pregled PDF-a">${iconFile()}</button>
|
||
<button class="ri-icon-btn-sm" onclick="window.open('/sport/api/v2/dokumenti/${doc.id}/text','_blank')" title="Parseni tekst">${iconDownload()}</button>
|
||
${doc.izvor_url ? `<a class="ri-icon-btn-sm" href="${doc.izvor_url}" target="_blank" title="Originalni izvor (web)">${iconExternal()}</a>` : ''}
|
||
</div></td>
|
||
</tr>`;
|
||
}
|
||
html += '</tbody></table>';
|
||
}
|
||
box.innerHTML = html;
|
||
} catch(e) { box.innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>'; }
|
||
}
|
||
|
||
async function dokSearch() {
|
||
const q = document.getElementById('dokQ').value.trim();
|
||
const r = document.getElementById('dokRazina').value || null;
|
||
const sp = document.getElementById('dokSport').value || null;
|
||
const box = document.getElementById('dokSearchResult');
|
||
if (!q) { box.innerHTML = ''; return; }
|
||
box.innerHTML = '<div class="muted">RAG vector search…</div>';
|
||
try {
|
||
const body = { q: q, limit: 8 };
|
||
if (r) body.razina = r;
|
||
if (sp) body.sport = sp;
|
||
const d = await fetch('/sport/api/v2/dokumenti/search', {
|
||
method:'POST', headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify(body)
|
||
}).then(x => x.json());
|
||
if (!d.results || !d.results.length) {
|
||
box.innerHTML = '<div class="muted">Nema RAG rezultata.</div>'; return;
|
||
}
|
||
let html = `<div class="muted" style="font-size:11px;margin-bottom:8px">RAG vector search — ${d.count} pogodaka:</div>`;
|
||
for (const r of d.results) {
|
||
html += `<div style="padding:10px;margin-bottom:8px;border:1px solid var(--border);border-radius:6px;background:var(--bg3)">
|
||
<div style="display:flex;justify-content:space-between;margin-bottom:4px">
|
||
<b style="cursor:pointer;color:var(--accent)" onclick="dokView(${r.dokument_id})">${r.naziv}</b>
|
||
<span class="muted" style="font-size:10px">score ${r.score}</span>
|
||
</div>
|
||
<div style="font-size:11px;color:var(--text2);margin-bottom:6px">${r.razina||''} · ${r.organizacija||''} · ${r.vrsta||''} ${r.sport ? '· '+r.sport : ''}</div>
|
||
<div style="font-size:12px;color:var(--text);line-height:1.5">${r.snippet||''}</div>
|
||
${r.izvor_url ? `<a href="${r.izvor_url}" target="_blank" class="muted" style="font-size:10px;margin-top:6px;display:inline-block">↗ ${r.izvor_url}</a>` : ''}
|
||
</div>`;
|
||
}
|
||
box.innerHTML = html;
|
||
} catch(e) { box.innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>'; }
|
||
}
|
||
|
||
async function dokAsk() {
|
||
const q = document.getElementById('dokAskQ').value.trim();
|
||
const box = document.getElementById('dokAskResult');
|
||
if (!q) return;
|
||
box.innerHTML = '<div class="muted">🤖 Hybrid AI agent razmišlja… (SQL + RAG)</div>';
|
||
try {
|
||
const d = await fetch('/sport/api/v2/ai/ask', {
|
||
method:'POST', headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify({q: q})
|
||
}).then(x => x.json());
|
||
|
||
const modeColor = {'SQL':'#10b981','RAG':'#7c3aed','BOTH':'#f59e0b','sql_error':'#dc2626','error':'#dc2626'}[d.mode]||'var(--accent)';
|
||
let html = `<div style="padding:14px;background:var(--bg3);border:1px solid var(--border);border-radius:8px;margin-top:8px">
|
||
<div style="font-size:11px;color:var(--text2);margin-bottom:10px;display:flex;justify-content:space-between">
|
||
<span>🤖 ODGOVOR (Hybrid AI)</span>
|
||
<span><span style="display:inline-block;padding:2px 8px;border-radius:3px;background:${modeColor};color:white;font-weight:600">${d.mode||'?'}</span> ${d.sql_count!==undefined?`<span class="muted">${d.sql_count} retka</span>`:''}</span>
|
||
</div>
|
||
<div style="white-space:pre-wrap;line-height:1.6;font-size:13px;color:var(--text-bright)">${(d.answer||'').replace(/</g,'<')}</div>
|
||
${d.sql ? `<details style="margin-top:10px"><summary class="muted" style="cursor:pointer;font-size:11px">▸ generated SQL</summary><pre style="font-size:10px;background:var(--bg);padding:8px;border-radius:3px;overflow-x:auto;color:var(--text2)">${d.sql.replace(/</g,'<')}</pre></details>` : ''}`;
|
||
|
||
if (d.sources && d.sources.length) {
|
||
html += `<div style="margin-top:14px;padding-top:12px;border-top:1px solid var(--border)">
|
||
<div style="font-size:11px;color:var(--text2);margin-bottom:8px">📚 IZVORI (RAG retrieval):</div>`;
|
||
for (const src of d.sources) {
|
||
html += `<div style="margin:4px 0;font-size:11px">
|
||
<span style="display:inline-block;width:24px;color:var(--accent);font-weight:600">[${src.n}]</span>
|
||
<b style="cursor:pointer;color:var(--accent)" onclick="dokView(${src.dokument_id||''})">${src.naziv||'?'}</b>
|
||
<span class="muted">· ${src.razina||''} · ${src.organizacija||''} · score ${src.score}</span>
|
||
${src.izvor_url ? ` <a href="${src.izvor_url}" target="_blank" class="muted">↗</a>` : ''}
|
||
</div>`;
|
||
}
|
||
html += '</div>';
|
||
}
|
||
if (d.sql) {
|
||
html += `<div style="margin-top:14px;padding-top:12px;border-top:1px solid var(--border)">
|
||
<div class="muted" style="font-size:11px;margin-bottom:6px">📜 SQL (auto-generated):</div>
|
||
<pre style="background:var(--bg);padding:8px;border-radius:4px;font-size:10px;overflow-x:auto;color:var(--text2);margin:0">${d.sql}</pre>
|
||
</div>`;
|
||
}
|
||
html += '</div>';
|
||
box.innerHTML = html;
|
||
} catch(e) { box.innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>'; }
|
||
}
|
||
|
||
async function dokView(did) {
|
||
if (!did) return;
|
||
try {
|
||
const d = await fetch('/sport/api/v2/dokumenti/' + did).then(x => x.json());
|
||
const doc = d.dokument;
|
||
let html = `<div class="card" style="padding:18px;margin-bottom:14px">
|
||
<button class="btn" onclick="pageDokumenti()" style="margin-bottom:10px;padding:4px 10px;font-size:11px">← natrag</button>
|
||
<h2 style="margin:0 0 6px 0">${doc.naziv}</h2>
|
||
<div class="muted" style="font-size:11px;margin-bottom:14px">${doc.razina||''} · ${doc.organizacija||''} · ${doc.vrsta||''} ${doc.sport ? '· '+doc.sport : ''}</div>
|
||
${doc.kratak_opis ? `<div style="margin-bottom:14px;padding:10px;background:var(--bg3);border-radius:6px;font-size:13px">${doc.kratak_opis}</div>` : ''}
|
||
<table style="width:100%;font-size:12px">
|
||
${doc.sluzbeni_glasnik ? `<tr><td class="muted" style="width:160px">Službeni glasnik</td><td><b>${doc.sluzbeni_glasnik}</b></td></tr>` : ''}
|
||
${doc.izdano_datum ? `<tr><td class="muted">Izdano</td><td>${doc.izdano_datum}</td></tr>` : ''}
|
||
${doc.izvor_url ? `<tr><td class="muted">Izvor</td><td><a href="${doc.izvor_url}" target="_blank" class="muted">${doc.izvor_url}</a></td></tr>` : ''}
|
||
${doc.kljucne_rijeci && doc.kljucne_rijeci.length ? `<tr><td class="muted">Ključne riječi</td><td>${doc.kljucne_rijeci.map(k=>`<span style='display:inline-block;padding:2px 8px;background:var(--bg4);border-radius:3px;font-size:10px;margin:0 4px 4px 0'>${k}</span>`).join('')}</td></tr>` : ''}
|
||
</table>
|
||
</div>`;
|
||
if (doc.sadrzaj && doc.sadrzaj.length > 100) {
|
||
html += `<div class="card" style="padding:18px;margin-bottom:14px">
|
||
<h4 style="margin:0 0 10px 0">📄 Puni sadržaj</h4>
|
||
<div style="white-space:pre-wrap;font-size:12px;line-height:1.6;max-height:600px;overflow-y:auto">${doc.sadrzaj.replace(/</g,'<')}</div>
|
||
</div>`;
|
||
}
|
||
if (d.chunks && d.chunks.length) {
|
||
html += `<div class="card" style="padding:18px">
|
||
<h4 style="margin:0 0 10px 0">🧩 Chunks (${d.chunks.length}) — embedded u Qdrant</h4>`;
|
||
for (const ch of d.chunks.slice(0,8)) {
|
||
html += `<div style="margin:8px 0;padding:8px;background:var(--bg3);border-radius:4px;font-size:11px;line-height:1.5">${(ch.chunk_text||'').slice(0,400)}…</div>`;
|
||
}
|
||
html += '</div>';
|
||
}
|
||
document.getElementById('content').innerHTML = html;
|
||
} catch(e) { alert('Greška: ' + e.message); }
|
||
}
|
||
|
||
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// PGŽ BAZA — agregat zaboravljenih tablica
|
||
// ═══════════════════════════════════════════════════════
|
||
let _bazaTab = 'objekti';
|
||
|
||
async function pageBaza() {
|
||
setTopbar('PGŽ baza znanja', 'Objekti · Natjecanja · Manifestacije · Najbolji · Potpore · Statistika · Vijesti');
|
||
const c = document.getElementById('content');
|
||
c.innerHTML = `
|
||
<div class="card" style="padding:0;margin-bottom:14px;overflow:hidden">
|
||
<div style="display:flex;border-bottom:1px solid var(--border);background:var(--bg3)">
|
||
<button class="bazaTab" data-t="objekti" onclick="bazaSetTab('objekti')">🏟️ Objekti</button>
|
||
<button class="bazaTab" data-t="natjecanja" onclick="bazaSetTab('natjecanja')">🏆 Natjecanja</button>
|
||
<button class="bazaTab" data-t="manifestacije" onclick="bazaSetTab('manifestacije')">🎉 Manifestacije</button>
|
||
<button class="bazaTab" data-t="najbolji" onclick="bazaSetTab('najbolji')">⭐ Najbolji</button>
|
||
<button class="bazaTab" data-t="potpore" onclick="bazaSetTab('potpore')">💰 Potpore</button>
|
||
<button class="bazaTab" data-t="statistika" onclick="bazaSetTab('statistika')">📊 Statistika</button>
|
||
<button class="bazaTab" data-t="vijesti" onclick="bazaSetTab('vijesti')">📰 Vijesti</button>
|
||
<button class="bazaTab" data-t="suci" onclick="bazaSetTab('suci')">👨⚖️ Suci</button>
|
||
<button class="bazaTab" data-t="treneri" onclick="bazaSetTab('treneri')">🎽 Treneri</button>
|
||
<button class="bazaTab" data-t="sponzori" onclick="bazaSetTab('sponzori')">🤝 Sponzori</button>
|
||
<button class="bazaTab" data-t="mediji" onclick="bazaSetTab('mediji')">📺 Mediji</button>
|
||
<button class="bazaTab" data-t="akademski" onclick="bazaSetTab('akademski')">🎓 Akademski</button>
|
||
<button class="bazaTab" data-t="hoo" onclick="bazaSetTab('hoo')">🏅 HOO kategorizirani</button>
|
||
<button class="bazaTab" data-t="stats2025" onclick="bazaSetTab('stats2025')">📊 Stats 2025</button>
|
||
</div>
|
||
<style>.bazaTab{padding:10px 16px;background:transparent;color:var(--text2);border:none;cursor:pointer;font-size:12px;border-right:1px solid var(--border)}.bazaTab.act{background:var(--bg);color:var(--accent);font-weight:600;border-bottom:2px solid var(--accent)}</style>
|
||
</div>
|
||
<div id="bazaBody"></div>`;
|
||
bazaSetTab(_bazaTab);
|
||
}
|
||
|
||
async function bazaSetTab(t) {
|
||
_bazaTab = t;
|
||
document.querySelectorAll('.bazaTab').forEach(b => b.classList.toggle('act', b.dataset.t === t));
|
||
const box = document.getElementById('bazaBody');
|
||
if (!box) return;
|
||
box.innerHTML = '<div class="card" style="padding:14px"><div class="muted">Učitavam…</div></div>';
|
||
try {
|
||
if (t === 'objekti') await bazaObjekti(box);
|
||
else if (t === 'natjecanja') await bazaNatjecanja(box);
|
||
else if (t === 'manifestacije') await bazaManifestacije(box);
|
||
else if (t === 'najbolji') await bazaNajbolji(box);
|
||
else if (t === 'potpore') await bazaPotpore(box);
|
||
else if (t === 'statistika') await bazaStatistika(box);
|
||
else if (t === 'vijesti') await bazaVijesti(box);
|
||
else if (t === 'suci') await bazaSuci(box);
|
||
else if (t === 'treneri') await bazaTreneri(box);
|
||
else if (t === 'sponzori') await bazaSponzori(box);
|
||
else if (t === 'mediji') await bazaMediji(box);
|
||
else if (t === 'akademski') await bazaAkademski(box);
|
||
else if (t === 'hoo') await bazaKategorizirani(box);
|
||
else if (t === 'stats2025') await bazaStats2025(box);
|
||
} catch(e) { box.innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>'; }
|
||
}
|
||
|
||
async function bazaObjekti(box) {
|
||
const d = await fetch('/sport/api/v2/objekti/list').then(r => r.json());
|
||
const grouped = {};
|
||
for (const o of d.results) (grouped[o.grad||'-'] = grouped[o.grad||'-']||[]).push(o);
|
||
let html = `<div class="card" style="padding:14px"><h3 style="margin:0 0 10px 0">🏟️ Sportski objekti PGŽ <span class="muted" style="font-size:11px">${d.count} objekata u ${Object.keys(grouped).length} gradova</span></h3>`;
|
||
for (const grad of Object.keys(grouped).sort()) {
|
||
html += `<div style="margin:14px 0"><h4 style="margin:0 0 6px 0;color:var(--accent)">${grad} <span class="muted" style="font-size:11px;font-weight:400">(${grouped[grad].length})</span></h4>`;
|
||
html += '<table class="ri-tbl ri-sortable" style="margin:0"><thead><tr><th>Naziv</th><th>Tip</th><th>Adresa</th><th>Upravitelj</th><th>Kapacitet</th><th>Sportovi</th><th>God</th><th>Linkovi</th></tr></thead><tbody>';
|
||
for (const o of grouped[grad]) {
|
||
const mapsQ = encodeURIComponent((o.naziv||'') + ' ' + (o.adresa||'') + ' ' + (o.grad||'') + ' Hrvatska');
|
||
const mapsURL = (o.lat && o.lng) ? `https://www.google.com/maps/search/?api=1&query=${o.lat},${o.lng}` : `https://www.google.com/maps/search/?api=1&query=${mapsQ}`;
|
||
const sportsHtml = (o.sportovi||[]).map(sp => `<span class="badge" style="cursor:pointer;background:var(--bg4);padding:1px 5px;margin:1px;border-radius:3px;font-size:10px" onclick="event.stopPropagation();state.searchQ='${sp.replace(/'/g,'')} ${o.grad||''}';state.page='search';render()">${sp}</span>`).join(' ');
|
||
html += `<tr style="cursor:pointer" onclick="window.open('${mapsURL}','_blank')" title="Klik = otvori na Google Maps">
|
||
<td><a href="${mapsURL}" target="_blank" style="color:var(--accent);text-decoration:none;font-weight:600" onclick="event.stopPropagation()">📍 ${o.naziv}</a></td>
|
||
<td><span class="badge" style="display:inline-block;padding:2px 6px;background:var(--bg4);border-radius:3px;font-size:10px;cursor:pointer" onclick="event.stopPropagation();state.searchQ='${(o.tip||'').replace(/'/g,'')} sportski objekt';state.page='search';render()">${o.tip}</span></td>
|
||
<td class="muted" style="font-size:11px"><a href="${mapsURL}" target="_blank" class="muted" onclick="event.stopPropagation()">${o.adresa||'-'}</a></td>
|
||
<td class="muted" style="font-size:11px;cursor:pointer" onclick="event.stopPropagation();state.searchQ='${(o.upravitelj||'').replace(/'/g,'')}';state.page='search';render()">${o.upravitelj||'-'}</td>
|
||
<td class="mono">${o.kapacitet ? Number(o.kapacitet).toLocaleString() : '-'}</td>
|
||
<td style="font-size:10px">${sportsHtml}</td>
|
||
<td class="mono">${o.izgradeno||'-'}</td>
|
||
<td onclick="event.stopPropagation()">${o.web ? `<a href="${o.web}" target="_blank" class="muted" title="Web">🌐</a> ` : ''}<a href="${mapsURL}" target="_blank" title="Google Maps">🗺️</a></td></tr>`;
|
||
}
|
||
html += '</tbody></table></div>';
|
||
}
|
||
html += '</div>';
|
||
box.innerHTML = html;
|
||
}
|
||
|
||
async function bazaNatjecanja(box) {
|
||
const d = await fetch('/sport/api/v2/natjecanja/list?limit=300').then(r => r.json());
|
||
const bySport = {};
|
||
for (const n of d.results) (bySport[n.sport||'-'] = bySport[n.sport||'-']||[]).push(n);
|
||
let html = `<div class="card" style="padding:14px"><h3 style="margin:0 0 10px 0">🏆 Natjecanja <span class="muted" style="font-size:11px">${d.count} natjecanja</span></h3>`;
|
||
for (const sport of Object.keys(bySport).sort()) {
|
||
html += `<div style="margin:10px 0"><h4 style="margin:0 0 6px 0">${sport} <span class="muted" style="font-size:11px;font-weight:400">(${bySport[sport].length})</span></h4>`;
|
||
html += '<table class="ri-tbl ri-sortable" style="margin:0"><thead><tr><th>Naziv</th><th>Razina</th><th>Tip</th><th>Sezona</th><th>Kategorija</th><th>Početak</th><th>Status</th></tr></thead><tbody>';
|
||
for (const n of bySport[sport].slice(0, 30)) {
|
||
const safeNaz = (n.naziv||'').replace(/'/g,'');
|
||
const linkNaz = n.external_url ? `<a href="${n.external_url}" target="_blank" style="color:var(--accent);text-decoration:none"><b>${n.naziv}</b></a>` : `<b style="cursor:pointer;color:var(--accent)" onclick="state.searchQ='${safeNaz}';state.page='search';render()">${n.naziv}</b>`;
|
||
html += `<tr>
|
||
<td>${linkNaz}</td>
|
||
<td class="muted" style="font-size:11px">${n.razina||'-'}</td>
|
||
<td class="muted" style="font-size:11px">${n.tip||'-'}</td>
|
||
<td class="mono" style="font-size:11px">${n.sezona||'-'}</td>
|
||
<td class="muted" style="font-size:10px">${n.kategorija||'-'}</td>
|
||
<td class="mono" style="font-size:11px">${n.datum_pocetka||'-'}</td>
|
||
<td><span style="display:inline-block;padding:2px 6px;border-radius:3px;font-size:10px;background:${n.status==='aktivno'?'#10b981':'#fb923c'};color:white">${n.status||'-'}</span></td></tr>`;
|
||
}
|
||
html += '</tbody></table></div>';
|
||
}
|
||
html += '</div>';
|
||
box.innerHTML = html;
|
||
}
|
||
|
||
async function bazaManifestacije(box) {
|
||
const d = await fetch('/sport/api/v2/manifestacije/list').then(r => r.json());
|
||
let html = `<div class="card" style="padding:14px"><h3 style="margin:0 0 10px 0">🎉 Manifestacije <span class="muted" style="font-size:11px">${d.count} manifestacija</span></h3>`;
|
||
html += '<table class="ri-tbl ri-sortable" style="margin:0"><thead><tr><th>Naziv</th><th>Mjesto</th><th>Organizator</th><th>Razina</th><th>Sudionici</th><th>Od godine</th><th>Savez</th><th>Maps</th></tr></thead><tbody>';
|
||
for (const m of d.results) {
|
||
const safeNaziv = (m.naziv||'').replace(/'/g,'');
|
||
const safeMjesto = (m.mjesto||'').replace(/'/g,'');
|
||
const safeSavez = (m.savez_naziv||'').replace(/'/g,'');
|
||
const safeOrg = (m.organizator||'').replace(/'/g,'');
|
||
const mapsQ = encodeURIComponent(safeMjesto + ' Hrvatska');
|
||
const mapsURL = `https://www.google.com/maps/search/?api=1&query=${mapsQ}`;
|
||
const searchURL = `https://www.google.com/search?q=${encodeURIComponent(safeNaziv + ' ' + safeMjesto)}`;
|
||
html += `<tr>
|
||
<td><a href="${searchURL}" target="_blank" style="color:var(--accent);text-decoration:none;font-weight:600" title="Pretraži manifestaciju">${m.naziv}</a></td>
|
||
<td><a href="${mapsURL}" target="_blank" class="muted" title="Otvori na Google Maps">📍 ${m.mjesto||'-'}</a></td>
|
||
<td class="muted" style="font-size:11px;cursor:pointer" onclick="state.searchQ='${safeOrg}';state.page='search';render()">${m.organizator||'-'}</td>
|
||
<td><span class="badge" style="display:inline-block;padding:2px 6px;background:var(--bg4);border-radius:3px;font-size:10px;cursor:pointer" onclick="state.searchQ='manifestacije ${(m.razina||'').replace(/'/g,'')}';state.page='search';render()">${m.razina||'-'}</span></td>
|
||
<td class="muted">${m.broj_ucesnika||'-'}</td>
|
||
<td class="mono">${m.godina_od||'-'}</td>
|
||
<td class="muted" style="font-size:11px;cursor:pointer" onclick="if(${m.savez_id||0}){state.searchQ='${safeSavez}';state.page='savezi';render()}else{state.searchQ='${safeSavez}';state.page='search';render()}">${m.savez_naziv ? `<span style="color:var(--accent)">${m.savez_naziv}</span>` : '-'}</td>
|
||
<td><a href="${mapsURL}" target="_blank" title="Google Maps">🗺️</a> <a href="${searchURL}" target="_blank" title="Web pretraga">🔍</a></td></tr>`;
|
||
}
|
||
html += '</tbody></table></div>';
|
||
box.innerHTML = html;
|
||
}
|
||
|
||
async function bazaNajbolji(box) {
|
||
const d = await fetch('/sport/api/v2/najbolji/list').then(r => r.json());
|
||
const byGod = {};
|
||
for (const n of d.results) (byGod[n.godina] = byGod[n.godina]||[]).push(n);
|
||
let html = `<div class="card" style="padding:14px"><h3 style="margin:0 0 10px 0">⭐ Najbolji sportaši PGŽ <span class="muted" style="font-size:11px">${d.count} priznanja kroz godine</span></h3>`;
|
||
for (const god of Object.keys(byGod).sort().reverse()) {
|
||
html += `<div style="margin:14px 0"><h4 style="margin:0 0 6px 0;color:var(--accent)">${god}</h4>`;
|
||
html += '<table class="ri-tbl ri-sortable" style="margin:0"><thead><tr><th>Kategorija</th><th>Ime</th><th>Klub</th><th>Sport</th></tr></thead><tbody>';
|
||
for (const n of byGod[god]) {
|
||
const safeIme = (n.ime_prezime||'').replace(/'/g,'');
|
||
const safeKlub = (n.klub||'').replace(/'/g,'');
|
||
const safeSport = (n.sport||'').replace(/'/g,'');
|
||
html += `<tr>
|
||
<td><b>${n.kategorija}</b></td>
|
||
<td style="cursor:pointer;color:var(--accent)" onclick="state.searchQ='${safeIme}';state.page='search';render()">${n.ime_prezime||'-'}</td>
|
||
<td class="muted" style="cursor:pointer" onclick="state.searchQ='${safeKlub}';state.page='search';render()">${n.klub||'-'}</td>
|
||
<td class="muted" style="font-size:11px;cursor:pointer" onclick="state.searchQ='${safeSport} PGŽ';state.page='search';render()">${n.sport||'-'}</td></tr>`;
|
||
}
|
||
html += '</tbody></table></div>';
|
||
}
|
||
html += '</div>';
|
||
box.innerHTML = html;
|
||
}
|
||
|
||
async function bazaPotpore(box) {
|
||
const summary = await fetch('/sport/api/v2/potpore/by-godina').then(r => r.json());
|
||
const all = await fetch('/sport/api/v2/potpore/list').then(r => r.json());
|
||
let html = `<div class="card" style="padding:14px"><h3 style="margin:0 0 10px 0">💰 Potpore nositeljima kvalitete <span class="muted" style="font-size:11px">${all.count} isplata · ${all.total_iznos.toLocaleString()} EUR ukupno</span></h3>`;
|
||
html += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin-bottom:14px">';
|
||
for (const s of summary.results) {
|
||
html += `<div style="padding:10px;background:var(--bg3);border:1px solid var(--border);border-radius:6px;text-align:center">
|
||
<div class="muted" style="font-size:11px">${s.godina}</div>
|
||
<div style="font-size:18px;font-weight:700;color:var(--accent)">${Number(s.ukupno).toLocaleString()} €</div>
|
||
<div class="muted" style="font-size:10px">${s.broj} potpora</div></div>`;
|
||
}
|
||
html += '</div>';
|
||
html += '<table class="ri-tbl ri-sortable" style="margin:0"><thead><tr><th>Godina</th><th>Klub</th><th>Sport</th><th>Iznos</th><th>Napomena</th></tr></thead><tbody>';
|
||
for (const p of all.results) {
|
||
const safeKlub = (p.naziv_kluba||'').replace(/'/g,'');
|
||
const safeSport = (p.sport||'').replace(/'/g,'');
|
||
html += `<tr>
|
||
<td class="mono">${p.godina}</td>
|
||
<td style="cursor:pointer" onclick="state.searchQ='${safeKlub}';state.page='search';render()"><b style="color:var(--accent)">${p.naziv_kluba}</b></td>
|
||
<td class="muted" style="font-size:11px;cursor:pointer" onclick="state.searchQ='${safeSport} klubovi PGŽ';state.page='search';render()">${p.sport||'-'}</td>
|
||
<td class="mono" style="text-align:right;font-weight:600">${Number(p.iznos).toLocaleString()} €</td>
|
||
<td class="muted" style="font-size:10px">${p.napomena||''}</td></tr>`;
|
||
}
|
||
html += '</tbody></table></div>';
|
||
box.innerHTML = html;
|
||
}
|
||
|
||
async function bazaStatistika(box) {
|
||
const d = await fetch('/sport/api/v2/statistika/list?godina=2024').then(r => r.json());
|
||
let html = `<div class="card" style="padding:14px"><h3 style="margin:0 0 10px 0">📊 Statistika saveza 2024 <span class="muted" style="font-size:11px">${d.count} saveza</span></h3>`;
|
||
html += '<table class="ri-tbl ri-sortable" style="margin:0"><thead><tr><th>Savez</th><th>Klubova članica</th><th>Kategoriziranih</th><th>Registriranih</th><th>Rekreativaca</th><th>Trenera</th><th>Reprezent.</th><th>Stipend.</th></tr></thead><tbody>';
|
||
for (const r of d.results) {
|
||
const safeSavez = (r.savez_naziv||'').replace(/'/g,'');
|
||
html += `<tr style="cursor:pointer" onclick="state.searchQ='${safeSavez}';state.page='savezi';render()">
|
||
<td><b style="color:var(--accent)">${r.savez_naziv}</b></td>
|
||
<td class="mono" style="text-align:right">${r.klubova_clanica}</td>
|
||
<td class="mono" style="text-align:right">${r.kategoriziranih}</td>
|
||
<td class="mono" style="text-align:right">${r.registriranih}</td>
|
||
<td class="mono" style="text-align:right">${r.rekreativaca}</td>
|
||
<td class="mono" style="text-align:right">${r.trenera}</td>
|
||
<td class="mono" style="text-align:right">${r.reprezentativaca}</td>
|
||
<td class="mono" style="text-align:right">${r.stipendiranih}</td></tr>`;
|
||
}
|
||
html += '</tbody></table></div>';
|
||
box.innerHTML = html;
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
async function bazaKategorizirani(box) {
|
||
const d = await fetch('/sport/api/v2/kategorizirani/list').then(r=>r.json());
|
||
const byKat = {};
|
||
for (const x of d.results) (byKat[x.hoo_kategorija]=byKat[x.hoo_kategorija]||[]).push(x);
|
||
let h = `<div class="card" style="padding:14px"><h3>🏅 HOO kategorizirani sportaši PGŽ <span class="muted" style="font-size:11px">${d.count} sportaša · izvor: Sportski godišnjak ZS PGŽ 2025</span></h3>`;
|
||
const labels = {'I':'I (vrhunski svjetski)','II':'II (međunarodni)','III':'III (državni)','IV':'IV (mladi)','V':'V (perspektivni)','VI':'VI (lokalni)'};
|
||
const colors = {'I':'#dc2626','II':'#fb923c','III':'#a855f7','IV':'#0ea5e9','V':'#10b981','VI':'#6b7280'};
|
||
for (const kat of Object.keys(byKat).sort()) {
|
||
h += `<div style="margin:14px 0">
|
||
<h4 style="margin:0 0 8px 0;color:${colors[kat]||'var(--accent)'}">${labels[kat]||kat} <span class="muted" style="font-size:11px;font-weight:400">(${byKat[kat].length})</span></h4>`;
|
||
h += '<table class="ri-tbl ri-sortable"><thead><tr><th>Ime</th><th>Sport</th><th>Klub</th><th>Mjesto</th><th>Vrijedi</th></tr></thead><tbody>';
|
||
for (const x of byKat[kat]) {
|
||
const safeImePrez = ((x.ime||'')+' '+(x.prezime||'')).replace(/'/g,'').trim();
|
||
const safeKlub = (x.klub_naziv||'').replace(/'/g,'');
|
||
const safeSport = (x.sport||'').replace(/'/g,'');
|
||
const safeMjesto = (x.mjesto_rodenja||'').replace(/'/g,'');
|
||
const mapsQ = encodeURIComponent(safeMjesto + ' Hrvatska');
|
||
h += `<tr>
|
||
<td style="cursor:pointer" onclick="state.searchQ='${safeImePrez}';state.page='search';render()"><b style="color:var(--accent)">${x.ime} ${x.prezime||''}</b></td>
|
||
<td><span class="badge" style="display:inline-block;padding:2px 6px;background:var(--bg4);border-radius:3px;font-size:10px;cursor:pointer" onclick="state.searchQ='${safeSport} PGŽ klubovi';state.page='search';render()">${x.sport||'-'}</span></td>
|
||
<td style="cursor:pointer" onclick="state.searchQ='${safeKlub}';state.page='search';render()">${x.klub_naziv||'-'}</td>
|
||
<td class="muted">${x.mjesto_rodenja ? `<a href="https://www.google.com/maps/search/?api=1&query=${mapsQ}" target="_blank" class="muted">📍 ${x.mjesto_rodenja}</a>` : '-'}</td>
|
||
<td class="muted" style="font-size:10px">${x.hoo_kategorija_od||''} - ${x.hoo_kategorija_do||''}</td>
|
||
</tr>`;
|
||
}
|
||
h += '</tbody></table></div>';
|
||
}
|
||
h += '</div>';
|
||
box.innerHTML = h;
|
||
}
|
||
|
||
async function bazaStats2025(box) {
|
||
const d = await fetch('/sport/api/v2/statistika-2025').then(r=>r.json());
|
||
let h = `<div class="card" style="padding:14px">
|
||
<h3>📊 Sportaši PGŽ 2025 po savezu <span class="muted" style="font-size:11px">${d.izvor}</span></h3>
|
||
<div style="margin:10px 0;font-size:11px;color:var(--text2)">Ukupno: <b>${d.ukupno}</b> sportaša pregledano u sportskoj ambulanti 2025.</div>`;
|
||
h += '<table class="ri-tbl ri-sortable"><thead><tr><th>Savez</th><th style="text-align:right">Registriranih</th><th>Bar</th></tr></thead><tbody>';
|
||
const max = Math.max(...d.results.map(r=>r.registriranih));
|
||
for (const r of d.results) {
|
||
const w = Math.round(r.registriranih / max * 100);
|
||
h += `<tr>
|
||
<td><b>${r.savez}</b></td>
|
||
<td class="mono" style="text-align:right;font-weight:600">${r.registriranih.toLocaleString()}</td>
|
||
<td><div style="width:200px;height:14px;background:var(--bg4);border-radius:4px"><div style="width:${w}%;height:100%;background:var(--accent);border-radius:4px"></div></div></td>
|
||
</tr>`;
|
||
}
|
||
h += '</tbody></table></div>';
|
||
box.innerHTML = h;
|
||
}
|
||
|
||
async function bazaSuci(box) {
|
||
const d = await fetch('/sport/api/v2/suci/list').then(r=>r.json());
|
||
const bySport = {};
|
||
for (const x of d.results) (bySport[x.sport]=bySport[x.sport]||[]).push(x);
|
||
let h = `<div class="card" style="padding:14px"><h3>👨⚖️ Suci PGŽ <span class="muted" style="font-size:11px">${d.count} sudaca u ${Object.keys(bySport).length} sportova</span></h3>`;
|
||
for (const sp of Object.keys(bySport).sort()) {
|
||
h += `<div style="margin:10px 0"><h4 style="margin:0 0 6px 0;color:var(--accent)">${sp} <span class="muted" style="font-size:11px;font-weight:400">(${bySport[sp].length})</span></h4>`;
|
||
h += '<table class="ri-tbl ri-sortable"><thead><tr><th>Ime</th><th>Licenca</th><th>Razina</th><th>Org</th><th>Grad</th></tr></thead><tbody>';
|
||
for (const x of bySport[sp]) {
|
||
const safeIP = ((x.ime||'')+' '+(x.prezime||'')).replace(/'/g,'').trim();
|
||
const safeOrg = (x.organizacija||'').replace(/'/g,'');
|
||
const safeGrad = (x.grad||'').replace(/'/g,'');
|
||
const mapsQ = encodeURIComponent(safeGrad + ' Hrvatska');
|
||
h += `<tr>
|
||
<td style="cursor:pointer" onclick="state.searchQ='${safeIP}';state.page='search';render()"><b style="color:var(--accent)">${x.ime} ${x.prezime||''}</b></td>
|
||
<td><span style="display:inline-block;padding:2px 6px;background:var(--bg4);border-radius:3px;font-size:10px">${x.licenca||'-'}</span></td>
|
||
<td class="muted" style="font-size:11px">${x.kategorija||'-'}</td>
|
||
<td class="muted" style="font-size:11px;cursor:pointer" onclick="state.searchQ='${safeOrg}';state.page='search';render()">${x.organizacija||'-'}</td>
|
||
<td class="muted">${x.grad ? `<a href="https://www.google.com/maps/search/?api=1&query=${mapsQ}" target="_blank" class="muted">📍 ${x.grad}</a>` : '-'}</td></tr>`;
|
||
}
|
||
h += '</tbody></table></div>';
|
||
}
|
||
h += '</div>';
|
||
box.innerHTML = h;
|
||
}
|
||
|
||
async function bazaTreneri(box) {
|
||
const d = await fetch('/sport/api/v2/treneri/list').then(r=>r.json());
|
||
let h = `<div class="card" style="padding:14px"><h3>🎽 Treneri PGŽ <span class="muted" style="font-size:11px">${d.count} trenera</span></h3>`;
|
||
h += '<table class="ri-tbl ri-sortable"><thead><tr><th>Sport</th><th>Ime</th><th>Klub</th><th>Pozicija</th><th>Licenca</th><th>Grad</th></tr></thead><tbody>';
|
||
for (const x of d.results) {
|
||
const safeIP = ((x.ime||'')+' '+(x.prezime||'')).replace(/'/g,'').trim();
|
||
const safeKlub = (x.klub_naziv||'').replace(/'/g,'');
|
||
const safeSport = (x.sport||'').replace(/'/g,'');
|
||
const safeGrad = (x.grad||'').replace(/'/g,'');
|
||
const mapsQ = encodeURIComponent(safeGrad + ' Hrvatska');
|
||
h += `<tr>
|
||
<td><span class="badge" style="display:inline-block;padding:2px 6px;background:var(--bg4);border-radius:3px;font-size:10px;cursor:pointer" onclick="state.searchQ='${safeSport} treneri PGŽ';state.page='search';render()">${x.sport}</span></td>
|
||
<td style="cursor:pointer" onclick="state.searchQ='${safeIP}';state.page='search';render()"><b style="color:var(--accent)">${x.ime} ${x.prezime||''}</b></td>
|
||
<td style="cursor:pointer" onclick="state.searchQ='${safeKlub}';state.page='search';render()">${x.klub_naziv||'-'}</td>
|
||
<td class="muted" style="font-size:11px">${x.pozicija||'-'}</td>
|
||
<td class="muted" style="font-size:11px">${x.licenca||'-'}</td>
|
||
<td class="muted">${x.grad ? `<a href="https://www.google.com/maps/search/?api=1&query=${mapsQ}" target="_blank" class="muted">📍 ${x.grad}</a>` : '-'}</td></tr>`;
|
||
}
|
||
h += '</tbody></table></div>';
|
||
box.innerHTML = h;
|
||
}
|
||
|
||
async function bazaSponzori(box) {
|
||
const d = await fetch('/sport/api/v2/sponzori/list').then(r=>r.json());
|
||
const byKlub = {};
|
||
for (const x of d.results) (byKlub[x.naziv_kluba]=byKlub[x.naziv_kluba]||[]).push(x);
|
||
let h = `<div class="card" style="padding:14px"><h3>🤝 Sponzori PGŽ <span class="muted" style="font-size:11px">${d.count} sponzorskih ugovora · ${Object.keys(byKlub).length} klubova</span></h3>`;
|
||
for (const klub of Object.keys(byKlub).sort()) {
|
||
h += `<div style="margin:10px 0"><h4 style="margin:0 0 6px 0;color:var(--accent)">${klub}</h4>`;
|
||
h += '<table class="ri-tbl ri-sortable"><thead><tr><th>Sponzor</th><th>Tip</th><th>Od</th><th>Iznos</th><th>Napomena</th></tr></thead><tbody>';
|
||
for (const x of byKlub[klub]) {
|
||
const safeSpon = (x.sponzor||'').replace(/'/g,'');
|
||
const searchURL = `https://www.google.com/search?q=${encodeURIComponent(safeSpon + ' Hrvatska')}`;
|
||
h += `<tr>
|
||
<td><a href="${searchURL}" target="_blank" style="color:var(--accent);text-decoration:none"><b>${x.sponzor}</b></a></td>
|
||
<td><span style="display:inline-block;padding:2px 6px;background:var(--bg4);border-radius:3px;font-size:10px">${x.tip||'-'}</span></td>
|
||
<td class="mono" style="font-size:11px">${x.razdoblje_od||'-'}</td>
|
||
<td class="mono">${x.iznos_eur ? Number(x.iznos_eur).toLocaleString()+' €' : '-'}</td>
|
||
<td class="muted" style="font-size:10px">${x.napomena||''}</td></tr>`;
|
||
}
|
||
h += '</tbody></table></div>';
|
||
}
|
||
h += '</div>';
|
||
box.innerHTML = h;
|
||
}
|
||
|
||
async function bazaMediji(box) {
|
||
const d = await fetch('/sport/api/v2/mediji/list').then(r=>r.json());
|
||
const byTip = {};
|
||
for (const x of d.results) (byTip[x.tip]=byTip[x.tip]||[]).push(x);
|
||
let h = `<div class="card" style="padding:14px"><h3>📺 Sportski mediji PGŽ <span class="muted" style="font-size:11px">${d.count} medija</span></h3>`;
|
||
for (const tip of Object.keys(byTip).sort()) {
|
||
h += `<div style="margin:10px 0"><h4 style="margin:0 0 6px 0;color:var(--accent)">${tip} <span class="muted" style="font-size:11px;font-weight:400">(${byTip[tip].length})</span></h4>`;
|
||
h += '<table class="ri-tbl ri-sortable"><thead><tr><th>Naziv</th><th>Grad</th><th>Vlasnik</th><th>Pokrivenost</th><th>Sport</th><th>Web</th></tr></thead><tbody>';
|
||
for (const x of byTip[tip]) {
|
||
const safeNaz = (x.naziv||'').replace(/'/g,'');
|
||
const safeGrad = (x.grad||'').replace(/'/g,'');
|
||
const safeVlas = (x.vlasnik||'').replace(/'/g,'');
|
||
const mapsQ = encodeURIComponent(safeGrad + ' Hrvatska');
|
||
const titleEl = x.web ? `<a href="${x.web}" target="_blank" style="color:var(--accent);text-decoration:none"><b>${x.naziv}</b></a>` : `<b style="cursor:pointer;color:var(--accent)" onclick="state.searchQ='${safeNaz}';state.page='search';render()">${x.naziv}</b>`;
|
||
h += `<tr>
|
||
<td>${titleEl}</td>
|
||
<td>${x.grad ? `<a href="https://www.google.com/maps/search/?api=1&query=${mapsQ}" target="_blank" class="muted">📍 ${x.grad}</a>` : '-'}</td>
|
||
<td class="muted" style="font-size:11px;cursor:pointer" onclick="state.searchQ='${safeVlas}';state.page='search';render()">${x.vlasnik||'-'}</td>
|
||
<td><span style="display:inline-block;padding:2px 6px;background:var(--bg4);border-radius:3px;font-size:10px">${x.pokrivenost||'-'}</span></td>
|
||
<td class="muted" style="font-size:11px">${(x.sport_fokus||[]).join(', ')}</td>
|
||
<td>${x.web ? `<a href="${x.web}" target="_blank" class="muted">↗</a>` : ''}</td></tr>`;
|
||
}
|
||
h += '</tbody></table></div>';
|
||
}
|
||
h += '</div>';
|
||
box.innerHTML = h;
|
||
}
|
||
|
||
async function bazaAkademski(box) {
|
||
const d = await fetch('/sport/api/v2/akademski/list').then(r=>r.json());
|
||
let h = `<div class="card" style="padding:14px"><h3>🎓 Akademski sport (UNIRI) <span class="muted" style="font-size:11px">${d.count} klubova</span></h3>`;
|
||
h += '<table class="ri-tbl ri-sortable"><thead><tr><th>Klub</th><th>Fakultet</th><th>Sport</th><th>Razina</th><th>Članova</th><th>Web</th></tr></thead><tbody>';
|
||
for (const x of d.results) {
|
||
const safeKlub = (x.naziv||'').replace(/'/g,'');
|
||
const safeFak = (x.fakultet||'').replace(/'/g,'');
|
||
const safeSport = (x.sport||'').replace(/'/g,'');
|
||
h += `<tr>
|
||
<td style="cursor:pointer" onclick="state.searchQ='${safeKlub}';state.page='search';render()"><b style="color:var(--accent)">${x.naziv}</b></td>
|
||
<td class="muted" style="cursor:pointer" onclick="state.searchQ='${safeFak}';state.page='search';render()">${x.fakultet||'-'}</td>
|
||
<td style="cursor:pointer" onclick="state.searchQ='${safeSport} klubovi';state.page='search';render()">${x.sport}</td>
|
||
<td><span style="display:inline-block;padding:2px 6px;background:var(--bg4);border-radius:3px;font-size:10px">${x.razina||'-'}</span></td>
|
||
<td class="mono">${x.broj_clanova||'-'}</td>
|
||
<td>${x.web ? `<a href="${x.web}" target="_blank" class="muted">↗</a>` : ''}</td></tr>`;
|
||
}
|
||
h += '</tbody></table></div>';
|
||
box.innerHTML = h;
|
||
}
|
||
|
||
async function bazaVijesti(box) {
|
||
const d = await fetch('/sport/api/v2/vijesti/list?limit=50').then(r => r.json());
|
||
let html = `<div class="card" style="padding:14px"><h3 style="margin:0 0 10px 0">📰 Vijesti <span class="muted" style="font-size:11px">${d.count} vijesti</span></h3>`;
|
||
for (const v of d.results) {
|
||
html += `<div style="padding:10px;border-bottom:1px solid var(--border)">
|
||
<div style="display:flex;justify-content:space-between;align-items:start">
|
||
<div style="flex:1">
|
||
<b>${v.naslov||'?'}</b>
|
||
${v.kategorija ? `<span style="display:inline-block;padding:2px 6px;background:var(--bg4);border-radius:3px;font-size:10px;margin-left:8px">${v.kategorija}</span>` : ''}
|
||
${v.sazetak ? `<div class="muted" style="font-size:11px;margin-top:4px">${v.sazetak.slice(0,200)}…</div>` : ''}
|
||
</div>
|
||
<div class="muted" style="font-size:11px;margin-left:14px">${v.datum||''}</div>
|
||
</div>
|
||
${v.url ? `<a href="${v.url}" target="_blank" class="muted" style="font-size:10px">↗ ${v.url.slice(0,60)}</a>` : ''}
|
||
</div>`;
|
||
}
|
||
html += '</div>';
|
||
box.innerHTML = html;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// FUNKCIONARI PGŽ — IO/NO/Skupštinari saveza i klubova
|
||
// ═══════════════════════════════════════════════════════
|
||
async function pageFunkcionari() {
|
||
const c = document.getElementById('content');
|
||
setTopbar('Funkcionari PGŽ', 'Sportski rukovoditelji — saveza, klubova, skupština');
|
||
c.innerHTML = `
|
||
<div class="card" style="margin-bottom:14px;padding:14px">
|
||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
||
<input id="fSearch" class="inp" placeholder="🔍 traži ime / prezime / funkcija / organizacija" style="flex:1;min-width:260px" oninput="fSearchType()">
|
||
<input id="fSportFilter" class="inp" placeholder="sport (npr. nogomet)" style="min-width:180px" oninput="fSearchType()">
|
||
<span class="muted" style="font-size:11px" id="fCount">—</span>
|
||
</div>
|
||
</div>
|
||
<div id="fGrid" class="card" style="padding:14px">Učitavam…</div>`;
|
||
await fLoad();
|
||
}
|
||
let _fT = null;
|
||
function fSearchType() { clearTimeout(_fT); _fT = setTimeout(fLoad, 300); }
|
||
async function fLoad() {
|
||
const q = (document.getElementById('fSearch')||{}).value || '';
|
||
const sp = (document.getElementById('fSportFilter')||{}).value || '';
|
||
const params = new URLSearchParams();
|
||
if (q) params.set('q', q);
|
||
if (sp) params.set('sport', sp);
|
||
params.set('limit', '300');
|
||
const grid = document.getElementById('fGrid');
|
||
try {
|
||
const d = await fetch('/sport/api/v2/osobe-funkcije/list?'+params.toString()).then(r=>r.json());
|
||
document.getElementById('fCount').textContent = `${d.count} funkcionara`;
|
||
if (!d.results || !d.results.length) {
|
||
grid.innerHTML = '<div class="muted" style="padding:40px;text-align:center">Nema rezultata.</div>';
|
||
return;
|
||
}
|
||
// Group by organizacija
|
||
const groups = {};
|
||
for (const r of d.results) {
|
||
const k = r.organizacija || r.savez_naziv || 'Ostali';
|
||
if (!groups[k]) groups[k] = [];
|
||
groups[k].push(r);
|
||
}
|
||
let html = '';
|
||
for (const [org, list] of Object.entries(groups)) {
|
||
html += `<div style="margin-bottom:18px">
|
||
<h3 style="margin-bottom:10px;color:var(--accent)">${org} <span class="muted" style="font-weight:400;font-size:11px">(${list.length})</span></h3>
|
||
<table class="ri-tbl ri-sortable"><thead><tr><th>#</th><th>Ime</th><th>Funkcija</th><th>Sport</th><th>Mandat</th><th>Izvor</th></tr></thead><tbody>`;
|
||
list.forEach((r,i) => {
|
||
const mandat = (r.mandate_od && r.mandate_do) ? `${r.mandate_od.slice(0,7)} → ${r.mandate_do.slice(0,7)}` : '—';
|
||
const url = r.izvor_url ? `<a href="${r.izvor_url}" target="_blank" class="muted" style="font-size:10px">${r.izvor||'link'}</a>` : (r.izvor||'—');
|
||
html += `<tr>
|
||
<td class="mono">${i+1}</td>
|
||
<td><b>${r.ime} ${r.prezime||''}</b></td>
|
||
<td>${r.funkcija||'—'}</td>
|
||
<td class="muted">${r.sport||''}</td>
|
||
<td class="mono" style="font-size:10px">${mandat}</td>
|
||
<td>${url}</td>
|
||
</tr>`;
|
||
});
|
||
html += '</tbody></table></div>';
|
||
}
|
||
grid.innerHTML = html;
|
||
} catch(e) { grid.innerHTML = '<div class="banner crit">Greška: '+e.message+'</div>'; }
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// SPORTSKI STATS — Top Scorers, Top Appearances, Klub Breakdown
|
||
// ═══════════════════════════════════════════════════════
|
||
async function pageSportStats() {
|
||
const c = document.getElementById('content');
|
||
setTopbar('Sport Stats', 'Pregled svih sportova PGŽ');
|
||
window.navStack = []; // reset breadcrumb na entry stranici
|
||
c.innerHTML = '<div class="card" style="padding:24px">Učitavam statistiku...</div>';
|
||
try {
|
||
const d = await fetch('/sport/api/v2/sport/svi/stats').then(r=>r.json());
|
||
const t = d.totals || {};
|
||
const sportovi = (d.sportovi || []).filter(s => s.sport && s.sport !== 'općenito');
|
||
|
||
let html = '';
|
||
// Top KPI summary
|
||
html += `<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin-bottom:14px">
|
||
${[
|
||
['Sportova', sportovi.length, '🏆'],
|
||
['Klubova', t.klubova || 0, '🏟️'],
|
||
['Sportaša', t.sportasa || 0, '🏃'],
|
||
['Saveza', t.saveza || 0, '📋'],
|
||
].map(([lbl,val,ico]) => `
|
||
<div class="ri-card" style="padding:14px;text-align:center">
|
||
<div style="font-size:22px;margin-bottom:4px">${ico}</div>
|
||
<div class="ri-kpi-value">${(val||0).toLocaleString('hr-HR')}</div>
|
||
<div class="ri-kpi-label">${lbl}</div>
|
||
</div>`).join('')}
|
||
</div>`;
|
||
|
||
// Grid svih sportova - klikabilan
|
||
html += `<div class="card" style="margin-bottom:14px">
|
||
<h3 style="margin-bottom:12px">Sportovi PGŽ — kliknite za detalje</h3>
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px">`;
|
||
sportovi.forEach(s => {
|
||
const ico = sportIcon(s.sport);
|
||
const isHns = s.sport === 'nogomet';
|
||
html += `<div class="ri-card" onclick="gotoSport('${s.sport.replace(/'/g, "'")}')"
|
||
style="cursor:pointer;padding:12px;transition:transform 0.15s,border-color 0.15s"
|
||
onmouseover="this.style.transform='translateY(-2px)';this.style.borderColor='var(--accent)'"
|
||
onmouseout="this.style.transform='';this.style.borderColor=''">
|
||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px">
|
||
<div style="font-size:28px">${ico}</div>
|
||
<div style="flex:1">
|
||
<div style="font-weight:600;font-size:14px;color:var(--text-bright);text-transform:capitalize">${s.sport}</div>
|
||
${isHns ? '<div style="font-size:9px;color:var(--accent)">HNS Semafor sync</div>' : ''}
|
||
</div>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;font-size:11px;color:var(--text2)">
|
||
<div><span class="muted">Klubova:</span> <b style="color:var(--text-bright)">${s.klubova||0}</b></div>
|
||
<div><span class="muted">Sportaša:</span> <b style="color:var(--text-bright)">${s.sportasa||0}</b></div>
|
||
<div><span class="muted">Saveza:</span> <b style="color:var(--text-bright)">${s.saveza||0}</b></div>
|
||
<div><span class="muted">Manif.:</span> <b style="color:var(--text-bright)">${s.manifestacija||0}</b></div>
|
||
${s.nagrada > 0 ? `<div style="grid-column:span 2"><span class="muted">Nagrada:</span> <b style="color:var(--amber)">${s.nagrada}</b></div>` : ''}
|
||
</div>
|
||
</div>`;
|
||
});
|
||
html += `</div></div>`;
|
||
|
||
c.innerHTML = html;
|
||
} catch(e) {
|
||
c.innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>';
|
||
}
|
||
}
|
||
|
||
function gotoSport(s) { state.page='sport'; state.sport_naziv = s; render(); }
|
||
|
||
async function pageSport() {
|
||
const sport = state.sport_naziv;
|
||
const c = document.getElementById('content');
|
||
setTopbar('Sport Stats', sport);
|
||
if (!sport) { c.innerHTML='<div class="banner crit">Nema sport.</div>'; return; }
|
||
c.innerHTML = '<div class="card" style="padding:24px">Učitavam ' + sport + '...</div>';
|
||
|
||
try {
|
||
const d = await fetch('/sport/api/v2/sport/' + encodeURIComponent(sport) + '/pregled').then(r=>r.json());
|
||
const ico = sportIcon(sport);
|
||
const st = d.stats || {};
|
||
|
||
let html = '';
|
||
|
||
// Breadcrumbs
|
||
html += breadcrumbs([
|
||
{label: '🏆 Sport Stats', onclick: "goto('sportStats')"},
|
||
{label: sportIcon(sport) + ' ' + sport.charAt(0).toUpperCase() + sport.slice(1)}
|
||
]);
|
||
|
||
// Header
|
||
html += `<div class="card" style="margin-bottom:14px;padding:18px;background:linear-gradient(135deg,var(--bg2),var(--bg3))">
|
||
<div style="display:flex;align-items:center;gap:18px">
|
||
<div style="font-size:54px">${ico}</div>
|
||
<div style="flex:1">
|
||
<h1 style="margin:0;text-transform:capitalize;color:var(--text-bright);font-size:26px">${sport}</h1>
|
||
<div class="muted" style="font-size:11px;margin-top:4px">Primorsko-goranska županija · ${st.broj_gradova||0} gradova</div>
|
||
</div>
|
||
<div onclick="goto('sportStats')" style="cursor:pointer;color:var(--accent);font-size:12px">← Svi sportovi</div>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:0;border-top:1px solid var(--border);margin-top:14px;padding-top:14px">
|
||
${[
|
||
['Klubova', st.broj_klubova, 'klubovi', null],
|
||
['Sportaša', st.broj_sportasa, 'sportasi', `M ${st.broj_sportasa_m||0} · Ž ${st.broj_sportasa_z||0}`],
|
||
['Kategoriziranih', st.broj_kategoriziranih, 'kategorizirani', `M ${st.broj_kategoriziranih_m||0} · Ž ${st.broj_kategoriziranih_z||0}`],
|
||
['Reprezentativaca', st.broj_reprezentativaca, 'reprezentativci', `M ${st.broj_reprezentativaca_m||0} · Ž ${st.broj_reprezentativaca_z||0}`],
|
||
['Saveza', d.savezi.length, 'savezi', null],
|
||
['Manifestacija', d.manifestacije.length, 'manifestacije', null],
|
||
].map(([lbl,v,filter,sub]) => `<div style="text-align:center;padding:6px;cursor:pointer;border-radius:6px;transition:background 0.2s" onmouseover="this.style.background='var(--bg3)'" onmouseout="this.style.background='transparent'" onclick="showSportasiModal('${sport}','${filter}','${lbl} u ${sport}')">
|
||
<div class="ri-kpi-value" style="font-size:20px">${(v||0).toLocaleString('hr-HR')}</div>
|
||
<div class="ri-kpi-label">${lbl}</div>
|
||
${sub ? `<div class="muted" style="font-size:10px;margin-top:2px">${sub}</div>` : ''}
|
||
</div>`).join('')}
|
||
</div>
|
||
</div>`;
|
||
|
||
// Saveze
|
||
if (d.savezi && d.savezi.length) {
|
||
html += `<div class="card" style="margin-bottom:14px"><h3 style="margin-bottom:10px">Savezi (${d.savezi.length})</h3>`;
|
||
d.savezi.forEach(s => {
|
||
html += `<div style="padding:8px 0;border-bottom:1px solid var(--border)">
|
||
<div style="font-weight:600">${s.naziv}</div>
|
||
<div class="muted" style="font-size:11px;margin-top:2px">
|
||
${s.predsjednik ? `Predsj.: ${s.predsjednik}` : ''}
|
||
${s.tajnik ? ` · Tajnik: ${s.tajnik}` : ''}
|
||
${s.godina_osnutka ? ` · od ${s.godina_osnutka}` : ''}
|
||
${s.grad ? ` · ${s.grad}` : ''}
|
||
</div>
|
||
</div>`;
|
||
});
|
||
html += `</div>`;
|
||
}
|
||
|
||
// Klubovi tabela
|
||
if (d.klubovi && d.klubovi.length) {
|
||
html += `<div class="card" style="margin-bottom:14px"><h3 style="margin-bottom:10px">Klubovi (${d.klubovi.length})</h3>
|
||
<table class="ri-tbl ri-sortable"><thead><tr><th>Klub</th><th>Razina</th><th>Grad</th><th>Osnovan</th>
|
||
<th style="text-align:right">Članova</th><th style="text-align:right">Kateg.</th></tr></thead><tbody>`;
|
||
d.klubovi.forEach(k => {
|
||
html += `<tr style="cursor:pointer" onclick="navPush();gotoKlubRoster(${k.id})" ondblclick="navPush();gotoKlubRoster(${k.id})" title="Klik za roster · Dvoklik za otvori">
|
||
<td><b>${k.naziv}</b>${k.hns_klub_id ? ' <span style="color:var(--accent);font-size:9px" title="Sinkroniziran s HNS Semafor COMET">HNS</span>' : ''}</td>
|
||
<td>${k.razina||'-'}</td>
|
||
<td>${k.grad||'-'}</td>
|
||
<td class="mono">${k.godina_osnutka||'-'}</td>
|
||
<td style="text-align:right">${k.broj_clanova||0}</td>
|
||
<td style="text-align:right;color:var(--accent)">${k.broj_kategoriziranih||0}</td>
|
||
</tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
}
|
||
|
||
// Top sportaši (kategorizirani / reprezentativci)
|
||
if (d.top_sportasi && d.top_sportasi.length) {
|
||
html += `<div class="card" style="margin-bottom:14px"><h3 style="margin-bottom:10px">Top osobe (kategorizirani sportaši, treneri, uprava) (${d.top_sportasi.length})</h3>
|
||
<table class="ri-tbl ri-sortable"><thead><tr><th>Foto</th><th>Ime i prezime</th><th>Uloga</th><th>Klub</th><th>Pozicija</th><th>HOO Kateg.</th><th>Repr.</th></tr></thead><tbody>`;
|
||
d.top_sportasi.forEach(s => {
|
||
html += `<tr style="cursor:pointer" onclick="navPush();gotoSportas(${s.id})" ondblclick="navPush();gotoSportas(${s.id})" title="Klik / Dvoklik za profil sportaša">
|
||
<td>${s.slika_url ? `<img src="${imgProxy(s.slika_url)}" style="width:32px;height:32px;border-radius:50%;object-fit:cover"/>` : '🏃'}</td>
|
||
<td><b>${s.ime||''} ${s.prezime||''}</b></td>
|
||
<td>${ulogaBadge(s.uloga)}</td>
|
||
<td>${s.klub_naziv||'-'}</td>
|
||
<td>${s.pozicija||'-'}</td>
|
||
<td><span class="risk-low">${s.kategorija_hoo ? 'I'.repeat(s.kategorija_hoo) : '-'}</span></td>
|
||
<td>${s.reprezentativac ? '<span class="risk-low">REPR</span>' : '-'}</td>
|
||
</tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
}
|
||
|
||
// Trofeji
|
||
if (d.trofeji && d.trofeji.length) {
|
||
html += `<div class="card" style="margin-bottom:14px"><h3 style="margin-bottom:10px">Povijesni trofeji (${d.trofeji.length})</h3>
|
||
<table class="ri-tbl ri-sortable"><thead><tr><th>Klub</th><th>Sezona</th><th>Natjecanje</th><th>Plasman</th><th>Trofej</th></tr></thead><tbody>`;
|
||
d.trofeji.forEach(t => {
|
||
html += `<tr ${t.klub_id?'style="cursor:pointer" onclick="gotoKlubRoster('+t.klub_id+')"':''}>
|
||
<td><b>${t.klub_naziv}</b></td>
|
||
<td class="mono">${t.sezona||'-'}</td>
|
||
<td>${t.natjecanje||'-'}</td>
|
||
<td style="text-align:center"><b style="color:${t.plasiranje==1?'gold':t.plasiranje==2?'silver':t.plasiranje==3?'#cd7f32':'var(--text2)'}">${t.plasiranje||'-'}.</b></td>
|
||
<td>${t.trofej||'-'}</td>
|
||
</tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
}
|
||
|
||
// Najbolji sportaši kroz godine
|
||
if (d.najbolji && d.najbolji.length) {
|
||
html += `<div class="card" style="margin-bottom:14px"><h3 style="margin-bottom:10px">Najbolji sportaši kroz godine (${d.najbolji.length})</h3>
|
||
<table class="ri-tbl ri-sortable"><thead><tr><th>Godina</th><th>Kategorija</th><th>Ime i prezime</th><th>Klub</th></tr></thead><tbody>`;
|
||
d.najbolji.forEach(n => {
|
||
html += `<tr>
|
||
<td class="mono"><b>${n.godina}</b></td>
|
||
<td>${n.kategorija||'-'}</td>
|
||
<td><b>${n.ime_prezime}</b></td>
|
||
<td class="muted">${n.klub||'-'}</td>
|
||
</tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
}
|
||
|
||
// Manifestacije
|
||
if (d.manifestacije && d.manifestacije.length) {
|
||
html += `<div class="card" style="margin-bottom:14px"><h3 style="margin-bottom:10px">Manifestacije (${d.manifestacije.length})</h3>
|
||
<table class="ri-tbl ri-sortable"><thead><tr><th>Naziv</th><th>Mjesto</th><th>Razina</th><th>Od godine</th><th>Učesnika</th><th>Organizator</th></tr></thead><tbody>`;
|
||
d.manifestacije.forEach(m => {
|
||
const safeNaz = (m.naziv||'').replace(/'/g,'');
|
||
const safeMjesto = (m.mjesto||'').replace(/'/g,'');
|
||
const safeOrg = (m.organizator||'').replace(/'/g,'');
|
||
const mapsQ = encodeURIComponent(safeMjesto + ' Hrvatska');
|
||
const searchURL = `https://www.google.com/search?q=${encodeURIComponent(safeNaz + ' ' + safeMjesto)}`;
|
||
html += `<tr>
|
||
<td><a href="${searchURL}" target="_blank" style="color:var(--accent);text-decoration:none"><b>${m.naziv}</b></a></td>
|
||
<td>${m.mjesto ? `<a href="https://www.google.com/maps/search/?api=1&query=${mapsQ}" target="_blank" class="muted">📍 ${m.mjesto}</a>` : '-'}</td>
|
||
<td>${m.razina||'-'}</td>
|
||
<td class="mono">${m.godina_od||'-'}</td>
|
||
<td>${m.broj_ucesnika||'-'}</td>
|
||
<td class="muted" style="font-size:11px;cursor:pointer" onclick="state.searchQ='${safeOrg}';state.page='search';render()">${m.organizator||'-'}</td>
|
||
</tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
}
|
||
|
||
if (!d.savezi.length && !d.klubovi.length && !d.top_sportasi.length) {
|
||
html += `<div class="card" style="padding:24px;text-align:center;color:var(--text3)">Nema podataka za sport "${sport}". Možda treba popuniti podatke.</div>`;
|
||
}
|
||
|
||
c.innerHTML = html;
|
||
} catch(e) {
|
||
c.innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>';
|
||
}
|
||
}
|
||
|
||
// (old pageSportStats removed)
|
||
|
||
async function spDoAdd() {
|
||
const promo_checks = document.querySelectorAll('input[name="spPromo"]:checked');
|
||
const promo_selected = Array.from(promo_checks).map(c => c.value);
|
||
const body = {
|
||
ime: document.getElementById('spIme').value.trim(),
|
||
prezime: document.getElementById('spPrezime').value.trim(),
|
||
klub_id: parseInt(document.getElementById('spKlub').value) || null,
|
||
datum_rodenja: document.getElementById('spDob').value || null,
|
||
mjesto_rodenja: document.getElementById('spMjesto').value.trim() || null,
|
||
broj_dresa: parseInt(document.getElementById('spDres').value) || null,
|
||
pozicija: document.getElementById('spPozicija').value || null,
|
||
dominantna_noga: document.getElementById('spNoga').value || null,
|
||
visina_cm: parseInt(document.getElementById('spVisina').value) || null,
|
||
tezina_kg: parseInt(document.getElementById('spTezina').value) || null,
|
||
slika_url: document.getElementById('spSlika').value.trim() || null,
|
||
oib: document.getElementById('spOib').value.trim() || null,
|
||
biografija: document.getElementById('spBio').value.trim() || null,
|
||
sport: (document.getElementById('spSport')||{}).value || null,
|
||
spol: (document.getElementById('spSpol')||{}).value || null,
|
||
promocija_kategorije: promo_selected,
|
||
};
|
||
const err = document.getElementById('spErr');
|
||
err.style.display = 'none';
|
||
if (!body.ime || !body.prezime || !body.klub_id) {
|
||
err.textContent = 'Ime, prezime i klub_id su obavezni';
|
||
err.style.display = 'block';
|
||
return;
|
||
}
|
||
try {
|
||
const d = await api('/api/v2/sportas/create', { method:'POST', body: JSON.stringify(body) });
|
||
alert('Kreiran sportaš #' + d.id);
|
||
usrModalClose();
|
||
if (typeof spLoad === 'function') spLoad();
|
||
if (typeof gotoSportas === 'function' && d.id) gotoSportas(d.id);
|
||
} catch(e) { err.textContent = e.message; err.style.display = 'block'; }
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// SPORTAŠI GRID — pretraživa galerija svih sportaša
|
||
// ═══════════════════════════════════════════════════════
|
||
async function pageSportasi() {
|
||
const c = document.getElementById('content');
|
||
setTopbar('Sportaši', 'Pregled svih sportaša PGŽ');
|
||
c.innerHTML = `
|
||
<div class="card" style="margin-bottom:14px;padding:14px">
|
||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
||
<input id="spSearch" class="inp" placeholder="🔍 traži po imenu, prezimenu" style="flex:1;min-width:240px" oninput="spSearchType()">
|
||
<select id="spKlubFilter" class="inp" style="min-width:200px" onchange="spLoad()">
|
||
<option value="">Svi klubovi</option>
|
||
</select>
|
||
<span class="muted" style="font-size:11px" id="spCount">—</span>
|
||
<button class="btn ri-btn-primary" onclick="spAddNew()">+ Novi sportaš</button>
|
||
</div>
|
||
</div>
|
||
<div id="spGrid" class="card" style="padding:14px">Učitavam…</div>
|
||
`;
|
||
await spLoadKlubovi();
|
||
await spLoad();
|
||
}
|
||
|
||
let _spTimer = null;
|
||
function spSearchType() { clearTimeout(_spTimer); _spTimer = setTimeout(spLoad, 300); }
|
||
|
||
async function spLoadKlubovi() {
|
||
try {
|
||
const r = await fetch('/sport/api/klubovi');
|
||
const d = await r.json();
|
||
const sel = document.getElementById('spKlubFilter');
|
||
if (!sel) return;
|
||
// Ispravi: uzmi niz iz različitih mogućih polja
|
||
let klubovi = [];
|
||
if (Array.isArray(d)) klubovi = d;
|
||
else if (d.results && Array.isArray(d.results)) klubovi = d.results;
|
||
else if (d.klubovi && Array.isArray(d.klubovi)) klubovi = d.klubovi;
|
||
else {
|
||
console.warn('Neočekivani format odgovora:', d);
|
||
klubovi = [];
|
||
}
|
||
if (klubovi.length) {
|
||
klubovi.sort((a,b) => (a.naziv||'').localeCompare(b.naziv||''));
|
||
sel.innerHTML = '<option value="">Svi klubovi</option>' +
|
||
klubovi.map(k => `<option value="${k.id}">${k.naziv}</option>`).join('');
|
||
} else {
|
||
sel.innerHTML = '<option value="">Svi klubovi</option>';
|
||
}
|
||
} catch(e) { console.error('klubovi load', e); }
|
||
}
|
||
|
||
async function spLoad() {
|
||
const q = (document.getElementById('spSearch')||{}).value || '';
|
||
const kid = (document.getElementById('spKlubFilter')||{}).value || '';
|
||
const params = new URLSearchParams();
|
||
if (q) params.set('q', q);
|
||
if (kid) params.set('klub_id', kid);
|
||
params.set('limit', '60');
|
||
|
||
const grid = document.getElementById('spGrid');
|
||
if (!grid) return;
|
||
|
||
try {
|
||
const d = await fetch('/sport/api/v2/sportas/search?' + params.toString()).then(r=>r.json());
|
||
document.getElementById('spCount').textContent = `${d.count} sportaša`;
|
||
if (!d.results || d.results.length === 0) {
|
||
grid.innerHTML = '<div class="muted" style="padding:40px;text-align:center">Nema rezultata. Promijeni filter ili pretragu.</div>';
|
||
return;
|
||
}
|
||
let html = '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px">';
|
||
for (const s of d.results) {
|
||
const dob = s.datum_rodenja ? new Date(s.datum_rodenja) : null;
|
||
const age = dob ? Math.floor((new Date() - dob) / (365.25 * 86400000)) : null;
|
||
const photo = s.slika_url || '';
|
||
html += `<div class="ri-card" onclick="gotoSportas(${s.id})" style="cursor:pointer;text-align:center;padding:12px;transition:transform 0.15s">
|
||
<div style="width:80px;height:80px;border-radius:50%;overflow:hidden;background:var(--bg4);margin:0 auto 8px;border:2px solid var(--border2)">
|
||
${photo ? `<img src="${imgProxy(photo)}" style="width:100%;height:100%;object-fit:cover" loading="lazy"/>` : '<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:30px;color:var(--text3)">🏃</div>'}
|
||
</div>
|
||
<div style="font-size:12px;font-weight:600;color:var(--text-bright)">${s.ime||''} ${s.prezime||''}</div>
|
||
<div class="muted" style="font-size:10px;margin-top:2px">${s.klub||'—'}</div>
|
||
${age ? `<div class="muted" style="font-size:10px">${age} g.</div>` : ''}
|
||
</div>`;
|
||
}
|
||
html += '</div>';
|
||
grid.innerHTML = html;
|
||
} catch(e) {
|
||
grid.innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>';
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// SPORTAŠ PROFIL — semafor.hns.family stil
|
||
// ═══════════════════════════════════════════════════════
|
||
function gotoSportas(id) { state.page='sportas'; state.sportas_id=id; render(); }
|
||
function gotoKlubRoster(id) { state.page='klubRoster'; state.klub_id=id; render(); }
|
||
|
||
async function pageSportas() {
|
||
const cid = state.sportas_id;
|
||
const c = document.getElementById("content");
|
||
setTopbar("Sportaši", "Profil sportaša #" + cid);
|
||
if (!cid) { c.innerHTML='<div class="banner crit">Nema ID sportaša.</div>'; return; }
|
||
c.innerHTML = '<div class="card" style="padding:24px">Učitavam profil…</div>';
|
||
try {
|
||
const d = await api('/api/v2/clanovi/'+cid+'/full-profile');
|
||
if (d.sezone && !d.seasons) d.seasons = d.sezone;
|
||
if (d.karijera && !d.career) d.career = d.karijera;
|
||
if (d.utakmice && !d.matches) d.matches = d.utakmice;
|
||
const sp = d.sportas;
|
||
const photo = sp.slika_url || '';
|
||
const dob = sp.datum_rodenja ? new Date(sp.datum_rodenja) : null;
|
||
const age = dob ? Math.floor((new Date() - dob) / (365.25 * 86400000)) : null;
|
||
let html = breadcrumbs([
|
||
{label: '🏃 Sportaši', onclick: "goto('sportasi')"},
|
||
sp.klub_naziv ? {label: '🏟️ ' + sp.klub_naziv, onclick: `navPush();gotoKlubRoster(${sp.klub_id})`} : null,
|
||
{label: (sp.ime || '?') + ' ' + (sp.prezime || '?')}
|
||
].filter(Boolean));
|
||
if (!sp.source_url || sp.source === 'manual') {
|
||
html += `<div class="banner" style="background:rgba(245,158,11,0.1);border:1px solid var(--amber);color:var(--amber);padding:8px 12px;margin-bottom:12px;border-radius:6px;font-size:12px">
|
||
⚠ Podaci o ovoj osobi su unijeti ručno i mogu biti nepotpuni. ${sp.source_url ? 'Izvor: <a href="'+sp.source_url+'" target="_blank">'+sp.source_url+'</a>' : 'Izvor nije naveden.'}
|
||
</div>`;
|
||
}
|
||
html += `<div class="card" style="padding:0;overflow:hidden;margin-bottom:14px">
|
||
<div style="display:grid;grid-template-columns:auto 1fr;gap:18px;padding:18px;background:linear-gradient(135deg,var(--bg2),var(--bg3))">
|
||
<div style="width:120px;height:120px;border-radius:50%;overflow:hidden;border:2px solid var(--border2);background:var(--bg4)">
|
||
${photo ? `<img src="${imgProxy(photo)}" style="width:100%;height:100%;object-fit:cover" alt="${sp.ime} ${sp.prezime}"/>` : '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text3);font-size:32px">🏃</div>'}
|
||
</div>
|
||
<div>
|
||
<h1 style="font-size:24px;font-weight:600;color:var(--text-bright);margin:0">${sp.ime||''} ${sp.prezime||''}</h1>
|
||
<div style="margin-top:6px">${ulogaBadge(sp.uloga)}</div>
|
||
<div style="display:flex;gap:18px;flex-wrap:wrap;margin-top:10px;font-size:12px;color:var(--text2)">
|
||
${sp.klub_naziv ? `<div onclick="navPush();gotoKlubRoster(${sp.klub_id})" style="cursor:pointer;color:var(--accent)" title="Klik za klub roster">${sportIcon(sp.sport)} ${sp.klub_naziv}</div>` : ''}
|
||
${sp.sport ? `<div>📍 ${sp.sport}</div>` : ''}
|
||
${sp.razina ? `<div class="mono" style="color:var(--text3)">${sp.razina}</div>` : ''}
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:14px;margin-top:18px">
|
||
<div><div class="ri-kpi-label">Datum rođenja</div><div style="font-size:13px;color:var(--text2)">
|
||
${dob ? dob.toLocaleDateString('hr-HR') + (age?' <span class="muted">('+age+' g)</span>':'') : (sp.godina_rodenja ? sp.godina_rodenja + ' <span class="muted">(samo godina)</span>' : '<span style="color:var(--text3);font-style:italic">— nepoznato —</span>')}
|
||
</div></div>
|
||
<div><div class="ri-kpi-label">Mjesto rođenja</div><div style="font-size:13px;color:var(--text2)">${sp.mjesto_rodenja || '<span style="color:var(--text3);font-style:italic">— nepoznato —</span>'}</div></div>
|
||
${sp.pozicija ? `<div><div class="ri-kpi-label">Pozicija</div><div style="font-size:13px;color:var(--text2)">${sp.pozicija}</div></div>`:''}
|
||
${sp.broj_dresa ? `<div><div class="ri-kpi-label">Broj dresa</div><div class="mono" style="font-size:18px;color:var(--accent);font-weight:700">${sp.broj_dresa}</div></div>`:''}
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// Dinamički KPI ovisno o sportu
|
||
const sport = (sp.sport || "").toLowerCase();
|
||
const totals = d.totals || {};
|
||
let kpis = [];
|
||
if (sport === "nogomet") kpis = [["Nastupi", totals.nastupa||0], ["Pogoci", totals.pogodaka||0, "var(--accent)"], ["Žuti", totals.zutih||0, "var(--amber)"], ["Crveni", totals.crvenih||0, "var(--red)"]];
|
||
else if (sport === "rukomet") kpis = [["Nastupi", totals.nastupa||0], ["Golovi", totals.pogodaka||0, "var(--accent)"], ["Asistencije", totals.asistencije||0], ["Isključenja", totals.iskljucenja||0, "var(--amber)"]];
|
||
else if (sport === "košarka" || sport === "kosarka") kpis = [["Nastupi", totals.nastupa||0], ["Poeni", totals.poeni||0, "var(--accent)"], ["Asistencije", totals.asistencije||0], ["Skokovi", totals.skokovi||0], ["Faulovi", totals.faulovi||0, "var(--amber)"]];
|
||
else if (sport === "odbojka") kpis = [["Nastupi", totals.nastupa||0], ["Poeni", totals.poeni||0, "var(--accent)"], ["Blokovi", totals.blokovi||0], ["Servisi", totals.servisi||0]];
|
||
else if (sport === "vaterpolo") kpis = [["Nastupi", totals.nastupa||0], ["Golovi", totals.pogodaka||0, "var(--accent)"], ["Isključenja", totals.iskljucenja||0, "var(--amber)"], ["Blokovi", totals.blokovi||0]];
|
||
else kpis = [["Nastupi", totals.nastupa||0], ["Ukupno", totals.pogodaka||0, "var(--accent)"]];
|
||
|
||
html += `<div style="display:grid;grid-template-columns:repeat(${kpis.length},1fr);gap:0;border-top:1px solid var(--border)">`;
|
||
kpis.forEach(k => { html += `<div style="padding:12px;text-align:center;border-right:1px solid var(--border)"><div class="ri-kpi-value" style="${k[2]?`color:${k[2]}`:""}">${k[1]}</div><div class="ri-kpi-label">${k[0]}</div></div>`; });
|
||
html += `</div></div>`;
|
||
|
||
if (d.seasons && d.seasons.length) {
|
||
html += `<div class="card" style="margin-bottom:14px"><h3 style="margin-bottom:10px">Statistika po sezonama</h3><table class="ri-tbl ri-sortable"><thead><tr>`;
|
||
if (sport === "nogomet") html += `<th>Sezona</th><th>Natjecanje</th><th style="text-align:right">Nastupi</th><th style="text-align:right">Pogoci</th><th style="text-align:right">Žuti</th><th style="text-align:right">Crveni</th><th style="text-align:right">Minute</th>`;
|
||
else if (sport === "rukomet") html += `<th>Sezona</th><th>Natjecanje</th><th style="text-align:right">Nastupi</th><th style="text-align:right">Golovi</th><th style="text-align:right">Asistencije</th><th style="text-align:right">Isključenja</th>`;
|
||
else if (sport === "košarka" || sport === "kosarka") html += `<th>Sezona</th><th>Natjecanje</th><th style="text-align:right">Nastupi</th><th style="text-align:right">Poeni</th><th style="text-align:right">Asistencije</th><th style="text-align:right">Skokovi</th><th style="text-align:right">Faulovi</th>`;
|
||
else if (sport === "odbojka") html += `<th>Sezona</th><th>Natjecanje</th><th style="text-align:right">Nastupi</th><th style="text-align:right">Poeni</th><th style="text-align:right">Blokovi</th><th style="text-align:right">Servisi</th>`;
|
||
else html += `<th>Sezona</th><th>Natjecanje</th><th style="text-align:right">Nastupi</th><th style="text-align:right">Ukupno</th>`;
|
||
html += `</tr></thead><tbody>`;
|
||
d.seasons.forEach(r => {
|
||
html += `<tr><td>${r.sezona||''}</td><td>${r.natjecanje||''}</td><td style="text-align:right">${r.nastupi}</td>`;
|
||
if (sport === "nogomet") html += `<td style="text-align:right;color:var(--accent)">${r.pogoci}</td><td style="text-align:right">${r.zuti}</td><td style="text-align:right">${r.crveni}</td><td style="text-align:right" class="mono">${r.minute_total||'-'}</td>`;
|
||
else if (sport === "rukomet") html += `<td style="text-align:right;color:var(--accent)">${r.pogoci}</td><td style="text-align:right">${r.asistencije||0}</td><td style="text-align:right">${r.iskljucenja||0}</td>`;
|
||
else if (sport === "košarka" || sport === "kosarka") html += `<td style="text-align:right;color:var(--accent)">${r.poeni||0}</td><td style="text-align:right">${r.asistencije||0}</td><td style="text-align:right">${r.skokovi||0}</td><td style="text-align:right">${r.faulovi||0}</td>`;
|
||
else if (sport === "odbojka") html += `<td style="text-align:right;color:var(--accent)">${r.poeni||0}</td><td style="text-align:right">${r.blokovi||0}</td><td style="text-align:right">${r.servisi||0}</td>`;
|
||
else html += `<td style="text-align:right;color:var(--accent)">${r.pogoci||0}</td>`;
|
||
html += `</tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
} else {
|
||
html += `<div class="card" style="margin-bottom:14px;padding:24px;text-align:center;color:var(--text3)">Nema zabilježenih utakmica. Podaci se osvježavaju iz HNS Semafor sustava (sezonski).</div>`;
|
||
}
|
||
if (d.career && d.career.length) {
|
||
html += `<div class="card" style="margin-bottom:14px"><h3 style="margin-bottom:10px">Karijera (klubovi)</h3><table class="ri-tbl ri-sortable"><thead><tr><th>Klub</th><th>Od</th><th>Do</th><th style="text-align:right">Nastupa</th></tr></thead><tbody>`;
|
||
d.career.forEach(r => {
|
||
html += `<tr style="cursor:pointer" onclick="navPush();gotoKlubRoster(${r.id})" title="Klik za klub"><td>${r.naziv}</td><td>${r.od_dat||'-'}</td><td>${r.do_dat||'-'}</td><td style="text-align:right">${r.nastupa}</td></tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
}
|
||
if (d.matches && d.matches.length) {
|
||
if (sport === "nogomet") {
|
||
html += `<div class="card"><h3 style="margin-bottom:10px">Posljednje utakmice (${d.matches.length})</h3><table class="ri-tbl ri-sortable"><thead><tr><th>Datum</th><th>Domaćin</th><th>:</th><th>Gost</th><th>Natjecanje</th><th style="text-align:right">Pogodaka</th><th>Kartoni</th><th style="text-align:right">Min</th></tr></thead><tbody>`;
|
||
d.matches.forEach(r => {
|
||
html += `<tr><td>${r.datum||'-'}</td><td>${r.klub_dom||'-'}</td><td class="mono" style="text-align:center;color:var(--text-bright)">${r.rezultat||''}</td><td>${r.klub_gost||'-'}</td><td>${r.natjecanje||''}</td><td style="text-align:right;color:var(--accent)">${r.pogodaka||0}</td><td><span class="risk-high">${r.zuti_kartoni||0}</span> <span class="risk-critical">${r.crveni_kartoni||0}</span></td><td style="text-align:right" class="mono">${r.minute||'-'}</td></tr>`;
|
||
});
|
||
} else {
|
||
html += `<div class="card"><h3 style="margin-bottom:10px">Posljednje utakmice (${d.matches.length})</h3><table class="ri-tbl ri-sortable"><thead><tr><th>Datum</th><th>Domaćin</th><th>:</th><th>Gost</th><th>Natjecanje</th></tr></thead><tbody>`;
|
||
d.matches.forEach(r => {
|
||
html += `<tr><td>${r.datum||'-'}</td><td>${r.klub_dom||'-'}</td><td class="mono">${r.rezultat||''}</td><td>${r.klub_gost||'-'}</td><td>${r.natjecanje||''}</td></tr>`;
|
||
});
|
||
}
|
||
html += `</tbody></table></div>`;
|
||
}
|
||
// A5_GUI_NAGRADE: HOO kategorija badge + medalje + povijesne nagrade
|
||
if (sp.kategorija_hoo) {
|
||
const kat_lbl = ['','I','II','III','IV','V','VI'][sp.kategorija_hoo] || sp.kategorija_hoo;
|
||
const kat_color = sp.kategorija_hoo===1?'var(--accent)':sp.kategorija_hoo===2?'#22c55e':sp.kategorija_hoo<=3?'#3b82f6':'var(--text2)';
|
||
// Inject HOO badge into header card (find first div after avatar, append)
|
||
// Easier: prepend a banner before everything below
|
||
html = html.replace(
|
||
'${sp.razina ? `<div class="mono"',
|
||
`<div style="background:${kat_color};color:#fff;padding:3px 10px;border-radius:12px;font-size:11px;font-weight:600">🏅 HOO KAT ${kat_lbl}</div>` +
|
||
'${sp.razina ? `<div class="mono"'
|
||
);
|
||
}
|
||
|
||
// Render nagrade (pojedinačne medalje 2025)
|
||
if (d.nagrade && d.nagrade.length) {
|
||
const sp_med = d.nagrade.filter(n => ['SP','EP','OI','SK'].includes(n.razina_natjecanja));
|
||
const dp_med = d.nagrade.filter(n => !['SP','EP','OI','SK'].includes(n.razina_natjecanja));
|
||
if (sp_med.length) {
|
||
html += `<div class="card" style="margin-bottom:14px;border-left:3px solid var(--accent)">
|
||
<h3 style="margin-bottom:10px">🏆 Međunarodne medalje (${sp_med.length})</h3>
|
||
<table class="ri-tbl ri-sortable"><thead><tr><th>Razina</th><th>Plasman</th><th>Natjecanje</th><th>Disciplina</th><th>Kategorija</th><th>Godina</th></tr></thead><tbody>`;
|
||
sp_med.forEach(n => {
|
||
const med_color = n.medalja==='ZLATO'?'#fbbf24':n.medalja==='SREBRO'?'#cbd5e1':n.medalja==='BRONCA'?'#d97706':'var(--text2)';
|
||
const med_emoji = n.medalja==='ZLATO'?'🥇':n.medalja==='SREBRO'?'🥈':n.medalja==='BRONCA'?'🥉':'';
|
||
html += `<tr><td><span class="mono" style="background:${med_color};color:#000;padding:2px 6px;border-radius:4px;font-size:10px;font-weight:700">${n.razina_natjecanja}</span></td>`;
|
||
html += `<td style="font-size:13px;color:${med_color};font-weight:600">${med_emoji} ${n.medalja||(n.plasman?n.plasman+'.':'-')}</td>`;
|
||
html += `<td>${n.natjecanje||'-'}</td><td>${n.disciplina||'-'}</td><td>${n.dobna_kategorija||'-'}</td><td class="mono">${n.godina||'-'}</td></tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
}
|
||
if (dp_med.length) {
|
||
html += `<div class="card" style="margin-bottom:14px">
|
||
<h3 style="margin-bottom:10px">🏅 Državne medalje (${dp_med.length})</h3>
|
||
<table class="ri-tbl ri-sortable"><thead><tr><th>Razina</th><th>Plasman</th><th>Natjecanje</th><th>Kategorija</th><th>Godina</th></tr></thead><tbody>`;
|
||
dp_med.forEach(n => {
|
||
const med_emoji = n.medalja==='ZLATO'?'🥇':n.medalja==='SREBRO'?'🥈':n.medalja==='BRONCA'?'🥉':'';
|
||
html += `<tr><td><span class="mono">${n.razina_natjecanja||'-'}</span></td>`;
|
||
html += `<td>${med_emoji} ${n.medalja||(n.plasman?n.plasman+'.':'-')}</td>`;
|
||
html += `<td>${n.natjecanje||'-'}</td><td>${n.dobna_kategorija||'-'}</td><td class="mono">${n.godina||'-'}</td></tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
}
|
||
}
|
||
|
||
// Render povijesne nagrade (najbolji_sportasi)
|
||
if (d.priznanja && d.priznanja.length) {
|
||
html += `<div class="card" style="margin-bottom:14px">
|
||
<h3 style="margin-bottom:10px">⭐ Priznanja po godinama (${d.priznanja.length})</h3>
|
||
<table class="ri-tbl ri-sortable"><thead><tr><th>Godina</th><th>Kategorija</th><th>Klub</th><th>Sport</th></tr></thead><tbody>`;
|
||
d.priznanja.forEach(p => {
|
||
html += `<tr><td class="mono"><b>${p.godina}</b></td><td>${p.kategorija||'-'}</td><td>${p.klub||'-'}</td><td>${p.sport||'-'}</td></tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
}
|
||
|
||
// Render klub_trofeji (klubske sezone)
|
||
if (d.klub_trofeji && d.klub_trofeji.length) {
|
||
html += `<div class="card" style="margin-bottom:14px">
|
||
<h3 style="margin-bottom:10px">🏟️ Trofeji kluba (${d.klub_trofeji.length})</h3>
|
||
<table class="ri-tbl ri-sortable"><thead><tr><th>Sezona</th><th>Natjecanje</th><th>Plasman</th><th>Trofej</th></tr></thead><tbody>`;
|
||
d.klub_trofeji.forEach(t => {
|
||
html += `<tr><td class="mono">${t.sezona||'-'}</td><td>${t.natjecanje||'-'}</td><td>${t.plasiranje?t.plasiranje+'.':'-'}</td><td style="color:var(--accent)">${t.trofej||'-'}</td></tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
}
|
||
|
||
if (sp.source_url) {
|
||
html += `<div style="margin-top:14px;font-size:10px;color:var(--text3);text-align:right">Izvor: <a href="${sp.source_url}" target="_blank" style="color:var(--accent)">${sp.source}</a> · Sync: ${sp.source_synced_at?new Date(sp.source_synced_at).toLocaleString('hr-HR'):'-'}</div>`;
|
||
}
|
||
// GODISNJAK_HISTORY_GUI - load and append history
|
||
try {
|
||
const gh = await api('/api/v2/sportas/'+sid+'/godisnjak_history');
|
||
if (gh && gh.count > 0) {
|
||
let ghHtml = `<div class="card" style="margin-top:14px">
|
||
<h3>📚 Godišnjaci ZS PGŽ — ${gh.count} godina pojavljivanja</h3>`;
|
||
gh.history.forEach(h => {
|
||
const m = h.has_medal ? '🥇' : '';
|
||
const k = h.has_kategorija ? '📋' : '';
|
||
const kw = (h.keywords||[]).slice(0,4).join(', ');
|
||
const snip = (h.snippet||'').replace(/[<>]/g,c=>({'<':'<','>':'>'}[c])).substring(0,500);
|
||
ghHtml += `<details style="margin-bottom:8px;padding:10px;background:rgba(255,255,255,0.03);border-radius:6px;border-left:3px solid var(--accent)">
|
||
<summary style="cursor:pointer;font-weight:600">
|
||
<span style="color:var(--accent)">${h.godina}</span> ${m} ${k}
|
||
${kw ? '<span class="muted" style="font-size:11px;margin-left:8px">['+kw+']</span>' : ''}
|
||
${h.izvor_url ? '<a href="'+h.izvor_url+'" target="_blank" style="float:right;font-size:11px;color:var(--accent)">PDF ↗</a>' : ''}
|
||
</summary>
|
||
<pre style="white-space:pre-wrap;font-size:11px;color:#bbb;margin-top:8px;font-family:monospace">${snip}</pre>
|
||
</details>`;
|
||
});
|
||
ghHtml += `</div>`;
|
||
html += ghHtml;
|
||
}
|
||
} catch(e) { console.warn('history load failed:', e); }
|
||
c.innerHTML = html;
|
||
} catch(e) { c.innerHTML = '<div class="banner crit">Greška: '+e.message+'</div>'; }
|
||
}
|
||
|
||
async function pageKlubRoster() {
|
||
const kid = state.klub_id;
|
||
const c = document.getElementById('content');
|
||
setTopbar('Klubovi', 'Roster kluba');
|
||
if (!kid) { c.innerHTML='<div class="banner crit">Nema ID kluba.</div>'; return; }
|
||
c.innerHTML = '<div class="card" style="padding:24px">Učitavam roster…</div>';
|
||
try {
|
||
const d = await api('/api/v2/klub/'+kid+'/sportasi');
|
||
const k = d.klub;
|
||
// Breadcrumb
|
||
let html = breadcrumbs([
|
||
{label: '🏟️ Klubovi', onclick: "goto('klubovi')"},
|
||
{label: k.sport ? sportIcon(k.sport) + ' ' + k.sport : '⚽', onclick: k.sport ? `goto('sport');state.sport_naziv='${k.sport}';render()` : null},
|
||
{label: k.naziv}
|
||
]);
|
||
|
||
html += `<div class="card" style="margin-bottom:14px">
|
||
<div style="display:flex;align-items:center;gap:14px">
|
||
${k.logo_url ? `<img src="${k.logo_url}" style="width:64px;height:64px;border-radius:8px"/>` : '<div style="width:64px;height:64px;border-radius:8px;background:var(--bg3);display:flex;align-items:center;justify-content:center;font-size:24px">⚽</div>'}
|
||
<div style="flex:1">
|
||
<h2 style="margin:0">${k.naziv}</h2>
|
||
<div class="muted" style="font-size:11px;margin-top:4px">${k.sport||''} · ${k.razina||''} · ${d.total} sportaša${k.hns_klub_id?' · <a href="https://semafor.hns.family/klubovi/'+k.hns_klub_id+'/'+(k.hns_slug||'')+'/" target="_blank" style="color:var(--accent)">HNS Semafor ↗</a>':''}</div>
|
||
</div>
|
||
${(state.user?.tip==='klub_admin' || state.user?.tip==='super_admin' || state.user?.tip==='pgz_admin') ? '<button class="btn-primary" onclick="addSportasPrompt('+kid+')">+ Sportaš</button>' : ''}
|
||
</div>
|
||
</div>`;
|
||
if (d.sportasi && d.sportasi.length) {
|
||
html += '<div class="card"><table class="ri-tbl ri-sortable"><thead><tr><th>#</th><th>Foto</th><th>Ime i prezime</th><th>Uloga</th><th>Datum rođenja</th><th>Pozicija</th><th style="text-align:right">Nast.</th><th style="text-align:right">Gol.</th><th>Izvor</th></tr></thead><tbody>';
|
||
d.sportasi.forEach(s => {
|
||
const dob = s.datum_rodenja ? new Date(s.datum_rodenja).toLocaleDateString('hr-HR') : '-';
|
||
html += `<tr style="cursor:pointer" onclick="navPush();gotoSportas(${s.id})" ondblclick="navPush();gotoSportas(${s.id})" title="Klik / Dvoklik za profil sportaša">
|
||
<td class="mono">${s.broj_dresa||'-'}</td>
|
||
<td>${s.slika_url ? `<img src="${imgProxy(s.slika_url)}" style="width:32px;height:32px;border-radius:50%;object-fit:cover"/>` : '🏃'}</td>
|
||
<td><b>${s.ime||''} ${s.prezime||''}</b> ${s.reprezentativac?'<span class="risk-low">REPR</span>':''}</td>
|
||
<td>${ulogaBadge(s.uloga)}</td>
|
||
<td>${dob}</td>
|
||
<td>${s.pozicija||'-'}</td>
|
||
<td style="text-align:right">${s.nastupa||0}</td>
|
||
<td style="text-align:right;color:var(--accent)">${s.pogoci||0}</td>
|
||
<td><span class="muted" style="font-size:10px">${s.source||'manual'}</span></td>
|
||
</tr>`;
|
||
});
|
||
html += '</tbody></table></div>';
|
||
} else {
|
||
html += '<div class="card" style="padding:24px;text-align:center;color:var(--text3)">Nema upisanih sportaša. Pokreni <code>hns_semafor.py</code> scraper ili dodaj ručno.</div>';
|
||
}
|
||
|
||
// A6_KLUBROSTER_PATCH: HOO sportaši, top medalisti, trofeji, povijesna priznanja
|
||
if (d.hoo_sportasi && d.hoo_sportasi.length) {
|
||
html += `<div class="card" style="margin-bottom:14px;border-left:3px solid var(--accent)">
|
||
<h3 style="margin-bottom:10px">🏅 HOO kategorizirani sportaši (${d.hoo_sportasi.length})</h3>
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px">`;
|
||
d.hoo_sportasi.forEach(h => {
|
||
const kat_lbl = ['','I','II','III','IV','V','VI'][h.kategorija_hoo] || h.kategorija_hoo;
|
||
const kat_color = h.kategorija_hoo===1?'var(--accent)':h.kategorija_hoo===2?'#22c55e':h.kategorija_hoo<=3?'#3b82f6':'var(--text2)';
|
||
html += `<div style="padding:8px 10px;background:var(--bg3);border-radius:6px;cursor:pointer;display:flex;justify-content:space-between;align-items:center" onclick="navPush();gotoSportas(${h.id})">
|
||
<div><b>${h.ime||''} ${h.prezime||''}</b><div class="muted" style="font-size:10px">${h.sport||''}</div></div>
|
||
<span style="background:${kat_color};color:#fff;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:700">KAT ${kat_lbl}</span>
|
||
</div>`;
|
||
});
|
||
html += `</div></div>`;
|
||
}
|
||
|
||
if (d.top_medalisti && d.top_medalisti.length) {
|
||
html += `<div class="card" style="margin-bottom:14px">
|
||
<h3 style="margin-bottom:10px">🥇 Najuspješniji sportaši kluba (${d.top_medalisti.length})</h3>
|
||
<table class="ri-tbl ri-sortable"><thead><tr><th>Sportaš</th><th style="text-align:right">🥇</th><th style="text-align:right">🥈</th><th style="text-align:right">🥉</th><th style="text-align:right">SP/EP/OI</th><th style="text-align:right">Ukupno</th></tr></thead><tbody>`;
|
||
d.top_medalisti.forEach(m => {
|
||
const click = m.clan_id ? `style="cursor:pointer" onclick="navPush();gotoSportas(${m.clan_id})"` : '';
|
||
html += `<tr ${click}><td><b>${m.ime_prezime||'?'}</b></td>
|
||
<td style="text-align:right;color:#fbbf24">${m.z||0}</td>
|
||
<td style="text-align:right;color:#cbd5e1">${m.s||0}</td>
|
||
<td style="text-align:right;color:#d97706">${m.b||0}</td>
|
||
<td style="text-align:right;color:var(--accent);font-weight:600">${m.svj||0}</td>
|
||
<td style="text-align:right;color:var(--text-bright);font-weight:600">${m.nagrade}</td>
|
||
</tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
}
|
||
|
||
if (d.trofeji && d.trofeji.length) {
|
||
html += `<div class="card" style="margin-bottom:14px">
|
||
<h3 style="margin-bottom:10px">🏆 Trofeji i sezonski plasmani (${d.trofeji.length})</h3>
|
||
<table class="ri-tbl ri-sortable"><thead><tr><th>Sezona</th><th>Natjecanje</th><th style="text-align:right">Plasman</th><th>Trofej</th><th>Napomena</th></tr></thead><tbody>`;
|
||
d.trofeji.forEach(t => {
|
||
const plac_emoji = t.plasiranje===1?'🥇':t.plasiranje===2?'🥈':t.plasiranje===3?'🥉':'';
|
||
html += `<tr><td class="mono">${t.sezona||'-'}</td>
|
||
<td>${t.natjecanje||'-'}</td>
|
||
<td style="text-align:right">${plac_emoji}${t.plasiranje||''}</td>
|
||
<td style="color:var(--accent);font-weight:600">${t.trofej||'-'}</td>
|
||
<td class="muted" style="font-size:11px">${t.napomena ? t.napomena.substring(0,80) : ''}</td>
|
||
</tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
}
|
||
|
||
if (d.priznanja && d.priznanja.length) {
|
||
html += `<div class="card" style="margin-bottom:14px">
|
||
<h3 style="margin-bottom:10px">⭐ Povijesna priznanja sportaša kluba (${d.priznanja.length})</h3>
|
||
<table class="ri-tbl ri-sortable"><thead><tr><th>Godina</th><th>Sportaš</th><th>Kategorija</th></tr></thead><tbody>`;
|
||
d.priznanja.forEach(p => {
|
||
const click = p.clan_id ? `style="cursor:pointer" onclick="navPush();gotoSportas(${p.clan_id})"` : '';
|
||
html += `<tr ${click}><td class="mono"><b>${p.godina}</b></td><td>${p.ime_prezime||'-'}</td><td>${p.kategorija||'-'}</td></tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
}
|
||
|
||
// C6_NATJECANJA_PATCH: liga natjecanja u kojima sudjeluje klub
|
||
try {
|
||
const dn = await api('/api/v2/klub/'+kid+'/natjecanja');
|
||
if (dn.natjecanja && dn.natjecanja.length) {
|
||
html += `<div class="card" style="margin-bottom:14px;border-left:3px solid var(--accent)">
|
||
<h3 style="margin-bottom:10px">⚽ Liga natjecanja (${dn.natjecanja.length})</h3>
|
||
<table class="ri-tbl ri-sortable"><thead><tr><th>Liga</th><th>Sezona</th><th>Razina</th><th style="text-align:right">Pozicija</th><th style="text-align:right">Odigrano</th><th style="text-align:right">P-N-Por</th><th style="text-align:right">Bodovi</th></tr></thead><tbody>`;
|
||
dn.natjecanja.forEach(n => {
|
||
const place_emoji = n.pozicija===1?'🥇':n.pozicija===2?'🥈':n.pozicija===3?'🥉':'';
|
||
html += `<tr style="cursor:pointer" onclick="goto('natjecanjaTablica');state.natj_id=${n.id};render()">
|
||
<td><b>${n.naziv||'-'}</b></td>
|
||
<td class="mono">${n.sezona||'-'}</td>
|
||
<td>${n.razina||'-'}</td>
|
||
<td style="text-align:right">${place_emoji}${n.pozicija||'-'}</td>
|
||
<td style="text-align:right">${n.odigrano||0}</td>
|
||
<td style="text-align:right;font-size:11px">${n.pobjede||0}-${n.nerijeseno||0}-${n.porazi||0}</td>
|
||
<td style="text-align:right;color:var(--accent);font-weight:600">${n.bodovi||0}</td>
|
||
</tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
}
|
||
} catch(_) {}
|
||
|
||
c.innerHTML = html;
|
||
} catch(e) { c.innerHTML = '<div class="banner crit">Greška: '+e.message+'</div>'; }
|
||
}
|
||
|
||
// C6_NATJECANJA_PATCH: pages
|
||
async function pageNatjecanja() {
|
||
const c = document.getElementById('content');
|
||
setTopbar('Natjecanja', 'Liga tablice');
|
||
c.innerHTML = '<div class="card" style="padding:24px">Učitavam natjecanja…</div>';
|
||
try {
|
||
const d = await api('/api/v2/natjecanja?pgz_only=true&limit=50');
|
||
let html = `<div class="card"><h2>⚽ Liga natjecanja (PGŽ relevant)</h2>
|
||
<p class="muted">${d.count} aktivnih liga / natjecanja sa PGŽ klubovima</p></div>`;
|
||
|
||
// Group by sport
|
||
const by_sport = {};
|
||
d.natjecanja.forEach(n => {
|
||
if (!by_sport[n.sport]) by_sport[n.sport] = [];
|
||
by_sport[n.sport].push(n);
|
||
});
|
||
|
||
for (const [sport, lige] of Object.entries(by_sport)) {
|
||
html += `<div class="card" style="margin-bottom:14px">
|
||
<h3 style="margin-bottom:10px">${sportIcon(sport)} ${sport} (${lige.length})</h3>
|
||
<table class="ri-tbl ri-sortable"><thead><tr><th>Liga</th><th>Sezona</th><th>Razina</th><th>Klubovi</th><th>Savez</th></tr></thead><tbody>`;
|
||
lige.forEach(n => {
|
||
html += `<tr style="cursor:pointer" onclick="state.natj_id=${n.id};goto('natjecanjaTablica')">
|
||
<td><b>${n.naziv}</b></td>
|
||
<td class="mono">${n.sezona||'-'}</td>
|
||
<td>${n.razina||'-'}</td>
|
||
<td style="text-align:right">${n.broj_klubova}</td>
|
||
<td class="muted">${n.savez_naziv||'-'}</td>
|
||
</tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
}
|
||
c.innerHTML = html;
|
||
} catch(e) { c.innerHTML = '<div class="banner crit">Greška: '+e.message+'</div>'; }
|
||
}
|
||
|
||
async function pageNatjecanjaTablica() {
|
||
const nid = state.natj_id;
|
||
const c = document.getElementById('content');
|
||
setTopbar('Natjecanja', 'Liga tablica');
|
||
if (!nid) { c.innerHTML='<div class="banner crit">Nema ID natjecanja.</div>'; return; }
|
||
c.innerHTML = '<div class="card" style="padding:24px">Učitavam tablicu…</div>';
|
||
try {
|
||
const d = await api('/api/v2/natjecanja/'+nid+'/tablica');
|
||
const n = d.natjecanje;
|
||
let html = breadcrumbs([
|
||
{label: '⚽ Natjecanja', onclick: "goto('natjecanja')"},
|
||
{label: n.naziv}
|
||
]);
|
||
html += `<div class="card" style="margin-bottom:14px">
|
||
<h2>${sportIcon(n.sport)} ${n.naziv}</h2>
|
||
<div class="muted" style="font-size:11px">${n.razina||''} · sezona ${n.sezona||''} · ${d.broj_klubova} klubova · <a href="${n.source_url||'#'}" target="_blank" style="color:var(--accent)">izvor ↗</a></div>
|
||
</div>`;
|
||
|
||
html += `<div class="card"><table class="ri-tbl ri-sortable">
|
||
<thead><tr><th>Poz</th><th>Klub</th><th style="text-align:right">Od</th><th style="text-align:right">P</th><th style="text-align:right">N</th><th style="text-align:right">Por</th><th style="text-align:right">+</th><th style="text-align:right">−</th><th style="text-align:right">+/−</th><th style="text-align:right">Bod</th></tr></thead>
|
||
<tbody>`;
|
||
d.tablica.forEach(t => {
|
||
const place_emoji = t.pozicija===1?'🥇':t.pozicija===2?'🥈':t.pozicija===3?'🥉':'';
|
||
const klub_link = t.klub_id ? `style="cursor:pointer" onclick="navPush();state.klub_id=${t.klub_id};goto('klubRoster')"` : '';
|
||
const klub_name = t.klub_naziv_db || t.klub_naziv;
|
||
const logo = t.logo_url ? `<img src="${t.logo_url}" style="width:18px;height:18px;border-radius:3px;vertical-align:middle;margin-right:6px"/>` : '';
|
||
html += `<tr ${klub_link}>
|
||
<td class="mono"><b>${place_emoji}${t.pozicija}</b></td>
|
||
<td>${logo}<b>${klub_name}</b>${t.klub_id?' <span class="muted" style="font-size:10px">↗</span>':''}</td>
|
||
<td style="text-align:right">${t.odigrano||0}</td>
|
||
<td style="text-align:right;color:#22c55e">${t.pobjede||0}</td>
|
||
<td style="text-align:right">${t.nerijeseno||0}</td>
|
||
<td style="text-align:right;color:#ef4444">${t.porazi||0}</td>
|
||
<td style="text-align:right">${t.gol_z||0}</td>
|
||
<td style="text-align:right">${t.gol_p||0}</td>
|
||
<td style="text-align:right">${(t.gol_razlika||0) > 0 ? '+' : ''}${t.gol_razlika||0}</td>
|
||
<td style="text-align:right;color:var(--accent);font-weight:700">${t.bodovi||0}</td>
|
||
</tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
c.innerHTML = html;
|
||
} catch(e) { c.innerHTML = '<div class="banner crit">Greška: '+e.message+'</div>'; }
|
||
}
|
||
|
||
|
||
async function addSportasPrompt(klub_id) {
|
||
const ime = prompt('Ime sportaša:'); if (!ime) return;
|
||
const prezime = prompt('Prezime sportaša:'); if (!prezime) return;
|
||
const broj = prompt('Broj dresa (opcionalno):') || null;
|
||
const pozicija = prompt('Pozicija (npr. Vratar / Igrač / Centarfor):') || null;
|
||
try {
|
||
const d = await api('/api/v2/sportas/create', { method:'POST', body: JSON.stringify({
|
||
klub_id, ime, prezime, broj_dresa: broj?parseInt(broj):null, pozicija
|
||
})});
|
||
alert('✓ Dodan sportaš ID '+d.id);
|
||
pageKlubRoster();
|
||
} catch(e) { alert('Greška: '+e.message); }
|
||
}
|
||
|
||
|
||
async function pageGodisnjaci() {
|
||
// DOKUMENTI_GUI_PATCH
|
||
const c = document.getElementById('content');
|
||
setTopbar('Dokumenti', 'Godišnjaci + savez novosti');
|
||
c.innerHTML = '<div class="card" style="padding:24px">Učitavam dokumente…</div>';
|
||
try {
|
||
const [godisnjaci, novosti] = await Promise.all([
|
||
api('/api/v2/dokumenti?vrsta=godisnjak&limit=30'),
|
||
api('/api/v2/dokumenti?vrsta=novost_savez&limit=30')
|
||
]);
|
||
|
||
let html = `<div class="card" style="margin-bottom:14px">
|
||
<h2>📚 Dokumenti & izvori</h2>
|
||
<p class="muted" style="font-size:11px">${godisnjaci.count} godišnjaka + ${novosti.count} savez novosti</p>
|
||
<div style="margin-top:12px;display:flex;gap:8px">
|
||
<input id="dokSearch" placeholder="Pretraga kroz 18 godišnjaka (2006-2024)…"
|
||
style="flex:1;padding:10px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);color:#fff;border-radius:6px"/>
|
||
<button onclick="searchDokumenti()" style="padding:10px 20px;background:var(--accent);color:#000;border:0;border-radius:6px;cursor:pointer">🔍 Traži</button>
|
||
</div>
|
||
<div style="display:flex;gap:6px;margin-top:8px;flex-wrap:wrap;font-size:11px">
|
||
<span class="muted">Brzi pretrag:</span>
|
||
<a onclick="state.dokQuery='Mirza Džomba';searchDokumenti()" style="color:var(--accent);cursor:pointer">Mirza Džomba</a>
|
||
<a onclick="state.dokQuery='HNK Rijeka';searchDokumenti()" style="color:var(--accent);cursor:pointer">HNK Rijeka</a>
|
||
<a onclick="state.dokQuery='Petar Klovar';searchDokumenti()" style="color:var(--accent);cursor:pointer">Petar Klovar</a>
|
||
<a onclick="state.dokQuery='Vitomir Maričić';searchDokumenti()" style="color:var(--accent);cursor:pointer">Vitomir Maričić</a>
|
||
<a onclick="state.dokQuery='Sara Kolak';searchDokumenti()" style="color:var(--accent);cursor:pointer">Sara Kolak</a>
|
||
<a onclick="state.dokQuery='RK Zamet';searchDokumenti()" style="color:var(--accent);cursor:pointer">RK Zamet</a>
|
||
<a onclick="state.dokQuery='najuspješniji';searchDokumenti()" style="color:var(--accent);cursor:pointer">najuspješniji</a>
|
||
</div>
|
||
<div id="dokSearchResults" style="margin-top:12px"></div>
|
||
</div>`;
|
||
|
||
// Godišnjaci grid
|
||
html += `<div class="card" style="margin-bottom:14px">
|
||
<h3 style="margin-bottom:10px">📖 Sportski godišnjaci ZS PGŽ (${godisnjaci.count})</h3>
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px">`;
|
||
godisnjaci.dokumenti.forEach(d => {
|
||
const chars = d.chars ? `${(d.chars/1000).toFixed(0)}k chars` : '';
|
||
html += `<div class="card" style="cursor:pointer;background:rgba(245,158,11,0.05);border-left:3px solid var(--accent);padding:14px"
|
||
onclick="state.dok_id=${d.id};goto('dokumentDetail')">
|
||
<div style="font-size:24px;font-weight:700;color:var(--accent)">${d.godina||'?'}</div>
|
||
<div style="font-size:11px;margin-top:4px">${d.organizacija||'-'}</div>
|
||
<div class="mono" style="font-size:10px;color:#aaa;margin-top:8px">${chars}</div>
|
||
</div>`;
|
||
});
|
||
html += `</div></div>`;
|
||
|
||
// Savez novosti
|
||
if (novosti.count > 0) {
|
||
html += `<div class="card" style="margin-bottom:14px">
|
||
<h3 style="margin-bottom:10px">📰 Savez novosti (PGŽ-relevant) (${novosti.count})</h3>
|
||
<table class="ri-tbl ri-sortable"><thead><tr><th>Datum</th><th>Naslov</th><th>Savez</th></tr></thead><tbody>`;
|
||
novosti.dokumenti.forEach(d => {
|
||
const dt = d.izdano_datum ? new Date(d.izdano_datum).toLocaleDateString('hr-HR') : '-';
|
||
html += `<tr style="cursor:pointer" onclick="window.open('${d.izvor_url}','_blank')">
|
||
<td class="mono" style="font-size:11px">${dt}</td>
|
||
<td><b>${d.title}</b></td>
|
||
<td class="muted" style="font-size:11px">${d.organizacija||'-'}</td>
|
||
</tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
}
|
||
c.innerHTML = html;
|
||
} catch(e) { c.innerHTML = '<div class="banner crit">Greška: '+e.message+'</div>'; }
|
||
}
|
||
|
||
window.searchDokumenti = async function() {
|
||
const q = state.dokQuery || (document.getElementById('dokSearch') ? document.getElementById('dokSearch').value : '');
|
||
if (!q || q.length < 2) return;
|
||
const target = document.getElementById('dokSearchResults');
|
||
if (!target) return;
|
||
target.innerHTML = '<p class="muted">Pretraga…</p>';
|
||
try {
|
||
const d = await api('/api/v2/dokumenti/search/q?q='+encodeURIComponent(q));
|
||
let html = `<h4 style="margin:12px 0 8px">📍 Rezultati za "${q}" (${d.count})</h4>`;
|
||
if (d.count === 0) {
|
||
html += `<p class="muted">Nema rezultata.</p>`;
|
||
} else {
|
||
html += `<div style="max-height:400px;overflow-y:auto">`;
|
||
d.rezultati.forEach(r => {
|
||
const yr = r.godina ? `<span style="color:var(--accent);font-weight:700">${r.godina}</span>` : '';
|
||
html += `<div style="margin-bottom:8px;padding:10px;background:rgba(255,255,255,0.03);border-radius:6px">
|
||
${yr} <b>${r.title}</b><br/>
|
||
<div class="mono" style="font-size:11px;margin-top:4px;color:#bbb;white-space:pre-wrap">${(r.excerpt||'').substring(0,500)}</div>
|
||
${r.izvor_url ? '<a href="'+r.izvor_url+'" target="_blank" style="font-size:11px;color:var(--accent)">izvor ↗</a>' : ''}
|
||
</div>`;
|
||
});
|
||
html += `</div>`;
|
||
}
|
||
target.innerHTML = html;
|
||
state.dokQuery = '';
|
||
} catch(e) { target.innerHTML = '<p style="color:#ef4444">Greška: '+e.message+'</p>'; }
|
||
}
|
||
|
||
async function pageDokumentDetail() {
|
||
// DOKUMENT_DETAIL
|
||
const did = state.dok_id;
|
||
const c = document.getElementById('content');
|
||
setTopbar('Dokument', 'Detalji');
|
||
if (!did) { c.innerHTML='<div class="banner crit">Nema ID dokumenta.</div>'; return; }
|
||
c.innerHTML = '<div class="card" style="padding:24px">Učitavam…</div>';
|
||
try {
|
||
const d = await api('/api/v2/dokumenti/'+did);
|
||
let html = breadcrumbs([
|
||
{label: '📚 Dokumenti', onclick: "goto('godisnjaci')"},
|
||
{label: d.title}
|
||
]);
|
||
html += `<div class="card" style="margin-bottom:14px">
|
||
<h2>${d.title}</h2>
|
||
<p class="muted" style="font-size:11px">
|
||
${d.organizacija||''} · ${d.godina||''} · ${d.vrsta||''} ·
|
||
${d.sadrzaj?d.sadrzaj.length.toLocaleString('hr-HR'):0} znakova
|
||
${d.izvor_url?'· <a href="'+d.izvor_url+'" target="_blank" style="color:var(--accent)">izvor PDF ↗</a>':''}
|
||
</p>
|
||
</div>`;
|
||
if (d.kratak_opis) {
|
||
html += `<div class="card" style="margin-bottom:14px"><h3>Sažetak</h3><p>${d.kratak_opis}</p></div>`;
|
||
}
|
||
if (d.sadrzaj) {
|
||
html += `<div class="card" style="margin-bottom:14px">
|
||
<h3>Sadržaj (${(d.sadrzaj.length/1000).toFixed(0)}k chars)</h3>
|
||
<pre style="white-space:pre-wrap;background:rgba(0,0,0,0.3);padding:14px;border-radius:6px;font-size:11px;max-height:600px;overflow-y:auto;font-family:monospace">${d.sadrzaj.substring(0,30000).replace(/[<>&]/g, c => ({'<':'<','>':'>','&':'&'}[c]))}${d.sadrzaj.length>30000?'\n\n…[truncated, '+d.sadrzaj.length+' total chars]':''}</pre>
|
||
</div>`;
|
||
}
|
||
c.innerHTML = html;
|
||
} catch(e) { c.innerHTML = '<div class="banner crit">Greška: '+e.message+'</div>'; }
|
||
}
|
||
|
||
|
||
async function pageCoverage() {
|
||
// COVERAGE_MATRIX_GUI
|
||
const c = document.getElementById('content');
|
||
setTopbar('Pokrivenost klubova', 'Heat-map kvalitete podataka');
|
||
c.innerHTML = '<div class="card" style="padding:24px">Učitavam pokrivenost…</div>';
|
||
try {
|
||
const d = await api('/api/v2/audit/coverage_matrix?limit=80');
|
||
|
||
// Heat-map function: 0 = red, max = green
|
||
const heat = (v, max) => {
|
||
if (!v || v === 0) return 'rgba(239,68,68,0.15)';
|
||
const r = v / Math.max(1,max);
|
||
if (r < 0.2) return 'rgba(239,68,68,0.4)';
|
||
if (r < 0.5) return 'rgba(245,158,11,0.4)';
|
||
if (r < 0.8) return 'rgba(132,204,22,0.4)';
|
||
return 'rgba(34,197,94,0.5)';
|
||
};
|
||
|
||
// Calculate maxes
|
||
const maxes = {
|
||
sportasa: Math.max(...d.klubovi.map(k => k.sportasa || 0)),
|
||
utakmica: Math.max(...d.klubovi.map(k => k.utakmica || 0)),
|
||
trofeja: Math.max(...d.klubovi.map(k => k.trofeja || 0)),
|
||
nagrada: Math.max(...d.klubovi.map(k => k.nagrada || 0)),
|
||
u_ligama: Math.max(...d.klubovi.map(k => k.u_ligama || 0)),
|
||
godina_god: 18
|
||
};
|
||
|
||
let html = `<div class="card" style="margin-bottom:14px">
|
||
<h2>🗂️ Heat-map pokrivenosti kluba</h2>
|
||
<p class="muted" style="font-size:11px">${d.count} aktivnih klubova · sortirano po kompozitnom skoru (sportaši + utakmice/10 + godišnjak×5)</p>
|
||
<div style="display:flex;gap:14px;margin-top:8px;font-size:11px">
|
||
<span><b style="color:#22c55e">●</b> ≥80%</span>
|
||
<span><b style="color:#84cc16">●</b> 50-80%</span>
|
||
<span><b style="color:#f59e0b">●</b> 20-50%</span>
|
||
<span><b style="color:#ef4444">●</b> <20%</span>
|
||
</div>
|
||
</div>`;
|
||
|
||
html += '<div class="card" style="overflow-x:auto">';
|
||
html += '<table class="ri-tbl ri-sortable"><thead><tr>' +
|
||
'<th style="position:sticky;left:0;background:#262624">Klub</th>' +
|
||
'<th>Sport</th>' +
|
||
'<th>Sportaši</th>' +
|
||
'<th>Utakmica</th>' +
|
||
'<th>Sezona</th>' +
|
||
'<th>Trofeja</th>' +
|
||
'<th>Nagrada</th>' +
|
||
'<th>U ligama</th>' +
|
||
'<th>Logo</th>' +
|
||
'<th>Godišnjak</th>' +
|
||
'<th>Period</th>' +
|
||
'</tr></thead><tbody>';
|
||
|
||
d.klubovi.forEach(k => {
|
||
const cell = (v, max) => `<td style="text-align:center;background:${heat(v, max)}">${v||0}</td>`;
|
||
const period = k.godisnjak_prvi && k.godisnjak_zadnji ?
|
||
`<span class="mono" style="font-size:10px">${k.godisnjak_prvi}-${k.godisnjak_zadnji}</span>` : '-';
|
||
const logo = k.ima_logo ? '<span style="color:#22c55e">✓</span>' : '<span style="color:#ef4444">✗</span>';
|
||
|
||
html += `<tr style="cursor:pointer" onclick="state.klub_id=${k.id};goto('klubRoster')">` +
|
||
`<td style="position:sticky;left:0;background:#262624;font-weight:600"><b>${k.naziv}</b>` +
|
||
(k.hns_klub_id ? `<br/><span class="mono" style="font-size:10px;color:#888">HNS#${k.hns_klub_id}</span>` : '') +
|
||
`</td>` +
|
||
`<td><span class="muted" style="font-size:11px">${k.sport||'-'}</span></td>` +
|
||
cell(k.sportasa, maxes.sportasa) +
|
||
cell(k.utakmica, maxes.utakmica) +
|
||
cell(k.sezona, 20) +
|
||
cell(k.trofeja, maxes.trofeja) +
|
||
cell(k.nagrada, maxes.nagrada) +
|
||
cell(k.u_ligama, maxes.u_ligama) +
|
||
`<td style="text-align:center">${logo}</td>` +
|
||
cell(k.godina_god, 18) +
|
||
`<td>${period}</td>` +
|
||
'</tr>';
|
||
});
|
||
html += '</tbody></table></div>';
|
||
|
||
c.innerHTML = html;
|
||
} catch(e) { c.innerHTML = '<div class="banner crit">Greška: '+e.message+'</div>'; }
|
||
}
|
||
|
||
async function pageAudit() {
|
||
// AUDIT_GUI_PATCH
|
||
const c = document.getElementById('content');
|
||
setTopbar('Audit', 'Status izvora podataka');
|
||
c.innerHTML = '<div class="card" style="padding:24px">Učitavam audit info…</div>';
|
||
try {
|
||
const [fresh, sources, coverage] = await Promise.all([
|
||
api('/api/v2/audit/freshness'),
|
||
api('/api/v2/audit/sources'),
|
||
api('/api/v2/audit/coverage')
|
||
]);
|
||
|
||
let html = `<div class="card" style="margin-bottom:14px">
|
||
<h2 style="margin:0">📊 Audit & izvori podataka</h2>
|
||
<p class="muted" style="font-size:11px">Posljednji upiti scrapera + distribucija izvora</p></div>`;
|
||
|
||
// Freshness
|
||
html += `<div class="card" style="margin-bottom:14px">
|
||
<h3 style="margin-bottom:10px">🕒 Aktualnost podataka</h3>
|
||
<table class="ri-tbl ri-sortable"><thead><tr><th>Tabela</th><th style="text-align:right">Broj</th><th>Zadnji update</th><th>Od zadnjeg</th></tr></thead><tbody>`;
|
||
fresh.freshness.forEach(f => {
|
||
const d = f.zadnji_update ? new Date(f.zadnji_update).toLocaleString('hr-HR') : '-';
|
||
const old = f.od_zadnjeg ? f.od_zadnjeg.split('.')[0] : '-';
|
||
const stale = f.zadnji_update && (Date.now() - new Date(f.zadnji_update)) > 7*86400000;
|
||
const route = auditRouteForTable(f.tabela);
|
||
html += `<tr style="cursor:pointer" onclick="${route}" title="Klik za otvaranje pregleda"><td><b>${f.tabela}</b></td><td style="text-align:right;color:var(--accent)">${(f.broj||0).toLocaleString('hr-HR')}</td><td class="mono" style="font-size:11px">${d}</td><td class="mono" style="${stale?'color:#ef4444':''};font-size:11px">${old}</td></tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
|
||
// Sources by tabela
|
||
html += `<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px">`;
|
||
|
||
html += `<div class="card"><h3 style="margin-bottom:10px">🏛️ Izvori klubova</h3>
|
||
<table class="ri-tbl ri-sortable"><thead><tr><th>Source</th><th style="text-align:right">Broj</th></tr></thead><tbody>`;
|
||
sources.klubovi_by_source.forEach(s => {
|
||
const r2 = sourceRoute('klubovi', s.source);
|
||
html += `<tr style="cursor:pointer" onclick="${r2}"><td><b>${s.source}</b></td><td style="text-align:right;color:var(--accent)">${s.broj.toLocaleString('hr-HR')}</td></tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
|
||
html += `<div class="card"><h3 style="margin-bottom:10px">👥 Izvori sportaši</h3>
|
||
<table class="ri-tbl ri-sortable"><thead><tr><th>Source</th><th style="text-align:right">Broj</th></tr></thead><tbody>`;
|
||
sources.clanovi_by_source.forEach(s => {
|
||
const r2 = sourceRoute('sportasi', s.source);
|
||
html += `<tr style="cursor:pointer" onclick="${r2}"><td><b>${s.source}</b></td><td style="text-align:right;color:var(--accent)">${s.broj.toLocaleString('hr-HR')}</td></tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
|
||
html += `<div class="card"><h3 style="margin-bottom:10px">⚽ Natjecanja</h3>
|
||
<table class="ri-tbl ri-sortable"><thead><tr><th>Source</th><th style="text-align:right">Broj</th></tr></thead><tbody>`;
|
||
sources.natjecanja_by_source.forEach(s => {
|
||
const r2 = sourceRoute('natjecanja', s.source);
|
||
html += `<tr style="cursor:pointer" onclick="${r2}"><td><b>${s.source}</b></td><td style="text-align:right;color:var(--accent)">${s.broj.toLocaleString('hr-HR')}</td></tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
|
||
html += `<div class="card"><h3 style="margin-bottom:10px">📄 Dokumenti</h3>
|
||
<table class="ri-tbl ri-sortable"><thead><tr><th>Vrsta</th><th style="text-align:right">Broj</th></tr></thead><tbody>`;
|
||
sources.dokumenti_by_vrsta.forEach(s => {
|
||
const r2 = sourceRoute('dokumenti', s.source);
|
||
html += `<tr style="cursor:pointer" onclick="${r2}"><td><b>${s.source}</b></td><td style="text-align:right;color:var(--accent)">${s.broj.toLocaleString('hr-HR')}</td></tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
|
||
html += `</div>`;
|
||
|
||
// Top klubovi by coverage
|
||
html += `<div class="card"><h3 style="margin-bottom:10px">🏆 Top klubovi po pokrivenosti (${coverage.count})</h3>
|
||
<table class="ri-tbl ri-sortable"><thead><tr><th>Klub</th><th>Sport</th><th style="text-align:right">Sportaša</th><th style="text-align:right">Utakmica</th><th style="text-align:right">Sezona</th><th style="text-align:right">Trofeja</th><th style="text-align:right">Nagrada</th><th>Source</th></tr></thead><tbody>`;
|
||
coverage.klubovi.slice(0, 100).forEach(k => {
|
||
html += `<tr style="cursor:pointer" onclick="state.klub_id=${k.id};goto('klubRoster')">
|
||
<td><b>${k.naziv}</b></td><td>${k.sport||'-'}</td>
|
||
<td style="text-align:right;color:var(--accent)">${k.sportasa||0}</td>
|
||
<td style="text-align:right">${k.utakmica||0}</td>
|
||
<td style="text-align:right">${k.sezona||0}</td>
|
||
<td style="text-align:right">${k.trofeja||0}</td>
|
||
<td style="text-align:right">${k.nagrada||0}</td>
|
||
<td class="mono" style="font-size:10px">${k.source||'-'}</td>
|
||
</tr>`;
|
||
});
|
||
html += `</tbody></table></div>`;
|
||
|
||
c.innerHTML = html;
|
||
} catch(e) { c.innerHTML = '<div class="banner crit">Greška: '+e.message+'</div>'; }
|
||
}
|
||
|
||
// MATRIX_PAGE - Audit Coverage Matrix heatmap
|
||
async function pageMatrix() {
|
||
const c = document.getElementById('content');
|
||
setTopbar('Coverage Matrix', 'Heat-map kvalitete podataka po klubu');
|
||
c.innerHTML = '<div class="card" style="padding:24px">Učitavam matricu…</div>';
|
||
try {
|
||
const params = new URLSearchParams(window.location.hash.split('?')[1] || '');
|
||
const minSize = params.get('min_size') || '5';
|
||
const sport = params.get('sport') || '';
|
||
|
||
const url = '/api/v2/audit/coverage-matrix?min_size=' + encodeURIComponent(minSize) + (sport ? '&sport=' + encodeURIComponent(sport) : '');
|
||
const r = await api(url);
|
||
const klubovi = r.klubovi || [];
|
||
|
||
// Sport filter dropdown
|
||
const sports = [...new Set(klubovi.map(k => k.sport).filter(Boolean))].sort();
|
||
|
||
// Color helper for heat
|
||
const heatColor = (v, max) => {
|
||
if (v === 0 || v == null) return 'var(--bg2)';
|
||
const t = Math.min(v / Math.max(max, 1), 1);
|
||
const r = Math.round(50 + (255-50) * (1-t));
|
||
const g = Math.round(50 + (220-50) * t);
|
||
return `rgb(${r},${g},80)`;
|
||
};
|
||
const scoreColor = (s) => {
|
||
if (s >= 70) return '#22c55e';
|
||
if (s >= 50) return '#eab308';
|
||
if (s >= 30) return '#f97316';
|
||
return '#ef4444';
|
||
};
|
||
|
||
// Max for each column for normalization
|
||
const maxSp = Math.max(...klubovi.map(k => k.sportasa || 0));
|
||
const maxUt = Math.max(...klubovi.map(k => k.utakmica || 0));
|
||
const maxTr = Math.max(...klubovi.map(k => k.trofeja || 0));
|
||
const maxGh = Math.max(...klubovi.map(k => k.god_hits || 0));
|
||
|
||
let html = `<div class="card" style="padding:18px;margin-bottom:14px">
|
||
<h2 style="margin:0 0 8px">📊 Coverage Matrix · Heat-map</h2>
|
||
<div class="muted" style="font-size:12px;margin-bottom:10px">${klubovi.length} klubova · weighted score (40% verified%, 20% utakmice, 15% sezona, 15% trofeja, 10% godišnjak hits)</div>
|
||
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center">
|
||
<span class="muted" style="font-size:12px">Min sportaša:</span>
|
||
${[1,5,10,20].map(n => `<button onclick="location.hash='matrix?min_size=${n}${sport?'&sport='+sport:''}'" style="padding:4px 10px;background:${minSize==n?'var(--accent)':'var(--bg2)'};border:1px solid var(--border);border-radius:4px;cursor:pointer;color:${minSize==n?'#000':'var(--text)'}">${n}+</button>`).join('')}
|
||
<span class="muted" style="font-size:12px;margin-left:12px">Sport:</span>
|
||
<select onchange="location.hash='matrix?min_size=${minSize}'+(this.value?'&sport='+encodeURIComponent(this.value):'')" style="padding:4px 8px;background:var(--bg2);border:1px solid var(--border);border-radius:4px;color:var(--text)">
|
||
<option value="">-- svi --</option>
|
||
${sports.map(s => `<option value="${s}" ${s===sport?'selected':''}>${s}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
</div>`;
|
||
|
||
// Color legend
|
||
html += `<div class="card" style="padding:10px;margin-bottom:10px;display:flex;gap:18px;flex-wrap:wrap;font-size:11px">
|
||
<span><b>Score:</b></span>
|
||
<span style="color:#22c55e">●</span> 70+ Excellent
|
||
<span style="color:#eab308">●</span> 50-69 Good
|
||
<span style="color:#f97316">●</span> 30-49 Sparse
|
||
<span style="color:#ef4444">●</span> <30 Empty
|
||
</div>`;
|
||
|
||
// Matrix table
|
||
html += `<div class="card" style="padding:0;overflow:auto">
|
||
<table class="data-table" style="margin:0;font-size:11px;width:100%">
|
||
<thead style="position:sticky;top:0;background:var(--bg)">
|
||
<tr>
|
||
<th style="text-align:left">Klub</th>
|
||
<th>Sport</th>
|
||
<th>Grad</th>
|
||
<th title="Ukupno sportaša u DB">Sportaša</th>
|
||
<th title="Postotak verificiranih (source_url)">Verified%</th>
|
||
<th title="Utakmice u utakmice_log">Utakmica</th>
|
||
<th title="Sezona u clan_sezona">Sezona</th>
|
||
<th title="Trofeji u klub_sezona">Trofeji</th>
|
||
<th title="Nagrade u clan_nagrada">Nagrade</th>
|
||
<th title="Hits u godišnjacima 2006-2024">Godišnjaci</th>
|
||
<th title="Weighted overall score">Score</th>
|
||
</tr>
|
||
</thead><tbody>`;
|
||
|
||
for (const k of klubovi) {
|
||
html += `<tr style="cursor:pointer" onclick="location.hash='klub/${k.klub_id}'">
|
||
<td style="text-align:left;font-weight:600">${k.naziv}</td>
|
||
<td>${k.sport || '—'}</td>
|
||
<td class="muted">${k.grad || '—'}</td>
|
||
<td style="background:${heatColor(k.sportasa, maxSp)};color:#000;font-weight:600;text-align:right">${k.sportasa}</td>
|
||
<td style="background:${heatColor(k.verified_pct, 100)};color:#000;font-weight:600">${k.verified_pct}%</td>
|
||
<td style="background:${heatColor(k.utakmica, maxUt)};color:#000">${k.utakmica}</td>
|
||
<td>${k.sezona}</td>
|
||
<td style="background:${heatColor(k.trofeja, maxTr)};color:#000">${k.trofeja}</td>
|
||
<td>${k.nagrada}</td>
|
||
<td style="background:${heatColor(k.god_hits, maxGh)};color:#000">${k.god_hits}</td>
|
||
<td style="background:${scoreColor(k.score)};color:#000;font-weight:700;text-align:center">${Math.round(k.score)}</td>
|
||
</tr>`;
|
||
}
|
||
|
||
html += `</tbody></table></div>`;
|
||
|
||
// Sports breakdown
|
||
const bySport = {};
|
||
for (const k of klubovi) {
|
||
const s = k.sport || 'unknown';
|
||
bySport[s] = bySport[s] || {count:0, scoreSum:0, sportasa:0, verified:0};
|
||
bySport[s].count++;
|
||
bySport[s].scoreSum += k.score;
|
||
bySport[s].sportasa += k.sportasa;
|
||
bySport[s].verified += k.verified;
|
||
}
|
||
|
||
html += `<div class="card" style="padding:18px;margin-top:14px">
|
||
<h3 style="margin:0 0 10px">📊 Po sportu</h3>
|
||
<table class="data-table" style="font-size:12px">
|
||
<tr><th>Sport</th><th>Klubova</th><th>Sportaša</th><th>Verified</th><th>Avg Score</th></tr>
|
||
${Object.entries(bySport).sort((a,b) => b[1].scoreSum/b[1].count - a[1].scoreSum/a[1].count).map(([s, v]) =>
|
||
`<tr>
|
||
<td><a href="#matrix?min_size=${minSize}&sport=${encodeURIComponent(s)}">${s}</a></td>
|
||
<td>${v.count}</td>
|
||
<td>${v.sportasa}</td>
|
||
<td>${v.verified}</td>
|
||
<td style="background:${scoreColor(v.scoreSum/v.count)};color:#000;font-weight:600">${Math.round(v.scoreSum/v.count)}</td>
|
||
</tr>`
|
||
).join('')}
|
||
</table>
|
||
</div>`;
|
||
|
||
c.innerHTML = html;
|
||
} catch (e) {
|
||
c.innerHTML = `<div class="card" style="padding:18px;color:#f87171">Greška: ${e}</div>`;
|
||
console.error(e);
|
||
}
|
||
}
|
||
|
||
|
||
buildNavs();
|
||
// Read URL hash on load
|
||
const _initHash = window.location.hash.slice(1);
|
||
if (_initHash && NAV.flatMap(s=>s.items||[]).some(i=>i.id===_initHash)) {
|
||
goto(_initHash);
|
||
}
|
||
goto('dashboard');
|
||
checkRole();
|
||
</script>
|
||
|
||
<!-- Coverage Matrix page (Sprint 3) -->
|
||
<div id="pageCoverage" class="page" style="display:none">
|
||
<div style="padding:20px">
|
||
<h1 style="color:var(--accent);margin-bottom:8px">Coverage Matrix</h1>
|
||
<p style="color:var(--text2);margin-bottom:16px">Heat-map pokrivenosti podataka po klubu. Score 0-100: 40% verified%, 20% utakmice, 15% sezone, 15% trofeji, 10% godišnjak.</p>
|
||
<div style="display:flex;gap:12px;align-items:center;margin-bottom:16px;flex-wrap:wrap">
|
||
<label style="color:var(--text2)">Min veličina kluba: <input type="number" id="cmMinSize" value="10" min="1" max="100" style="width:60px;padding:4px;background:var(--card2);border:1px solid var(--border);color:var(--text);border-radius:4px"/></label>
|
||
<label style="color:var(--text2)">Sport: <input type="text" id="cmSport" placeholder="(svi)" style="width:140px;padding:4px;background:var(--card2);border:1px solid var(--border);color:var(--text);border-radius:4px"/></label>
|
||
<button onclick="loadCoverageMatrix()" style="padding:6px 14px;background:var(--accent);color:#000;border:none;border-radius:6px;cursor:pointer;font-weight:600">Učitaj</button>
|
||
<span id="cmCount" style="color:var(--text2)"></span>
|
||
</div>
|
||
<div id="cmTable" style="overflow-x:auto"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
async function loadCoverageMatrix() {
|
||
const min = document.getElementById('cmMinSize').value || 10;
|
||
const sport = document.getElementById('cmSport').value || '';
|
||
const params = new URLSearchParams({min_size: min});
|
||
if (sport) params.append('sport', sport);
|
||
document.getElementById('cmTable').innerHTML = '<div style="padding:20px;color:var(--text2)">Učitavam...</div>';
|
||
try {
|
||
const r = await fetch('/sport/api/v2/audit/coverage-matrix?' + params);
|
||
const d = await r.json();
|
||
document.getElementById('cmCount').innerText = d.count + ' klubova';
|
||
const klubovi = d.klubovi || [];
|
||
if (!klubovi.length) {
|
||
document.getElementById('cmTable').innerHTML = '<div style="padding:20px;color:var(--text2)">Nema rezultata.</div>';
|
||
return;
|
||
}
|
||
|
||
function cell(val, max) {
|
||
const pct = max > 0 ? Math.min(100, (val / max) * 100) : 0;
|
||
const hue = pct * 1.2; // 0=red, 120=green
|
||
const bg = `hsl(${hue}, 70%, 22%)`;
|
||
return `<td style="background:${bg};color:#fff;padding:6px 10px;text-align:center;font-weight:600">${val||0}</td>`;
|
||
}
|
||
function scoreCell(score) {
|
||
const hue = Math.min(120, score * 1.2);
|
||
const bg = `hsl(${hue}, 80%, 30%)`;
|
||
return `<td style="background:${bg};color:#fff;padding:6px 10px;text-align:center;font-weight:700;font-size:14px">${Math.round(score)}</td>`;
|
||
}
|
||
|
||
const maxes = {
|
||
sportasa: Math.max(...klubovi.map(k=>k.sportasa)),
|
||
verified: 100,
|
||
utakmica: Math.max(...klubovi.map(k=>k.utakmica)),
|
||
sezona: Math.max(...klubovi.map(k=>k.sezona), 1),
|
||
trofeja: Math.max(...klubovi.map(k=>k.trofeja), 1),
|
||
god: Math.max(...klubovi.map(k=>k.god_hits), 1),
|
||
};
|
||
|
||
let html = '<table style="width:100%;border-collapse:collapse;font-size:12px"><thead><tr style="background:var(--card2)">';
|
||
html += '<th style="padding:8px;text-align:left;color:var(--text2)">Klub</th>';
|
||
html += '<th style="padding:8px;color:var(--text2)">Sport</th>';
|
||
html += '<th style="padding:8px;color:var(--text2)">Sportaši</th>';
|
||
html += '<th style="padding:8px;color:var(--text2)">Verified%</th>';
|
||
html += '<th style="padding:8px;color:var(--text2)">Utakmica</th>';
|
||
html += '<th style="padding:8px;color:var(--text2)">Sezona</th>';
|
||
html += '<th style="padding:8px;color:var(--text2)">Trofeja</th>';
|
||
html += '<th style="padding:8px;color:var(--text2)">Godišnjak</th>';
|
||
html += '<th style="padding:8px;color:var(--text2)">Score</th>';
|
||
html += '</tr></thead><tbody>';
|
||
|
||
for (const k of klubovi) {
|
||
html += '<tr style="border-bottom:1px solid var(--border)">';
|
||
html += `<td style="padding:8px"><a href="#" onclick="loadKlub(${k.klub_id});showPage('pageKlubDetail');return false" style="color:var(--accent);text-decoration:none">${k.naziv}</a></td>`;
|
||
html += `<td style="padding:8px;color:var(--text2)">${k.sport||'-'}</td>`;
|
||
html += cell(k.sportasa, maxes.sportasa);
|
||
html += cell(k.verified_pct, maxes.verified);
|
||
html += cell(k.utakmica, maxes.utakmica);
|
||
html += cell(k.sezona, maxes.sezona);
|
||
html += cell(k.trofeja, maxes.trofeja);
|
||
html += cell(k.god_hits, maxes.god);
|
||
html += scoreCell(k.score);
|
||
html += '</tr>';
|
||
}
|
||
html += '</tbody></table>';
|
||
document.getElementById('cmTable').innerHTML = html;
|
||
} catch (e) {
|
||
document.getElementById('cmTable').innerHTML = '<div style="padding:20px;color:#f55">Greška: ' + e.message + '</div>';
|
||
}
|
||
}
|
||
|
||
// Load on page show
|
||
const _origShowPageCM = typeof showPage === 'function' ? showPage : null;
|
||
window.addEventListener('load', () => {
|
||
// hook
|
||
const orig = window.showPage;
|
||
if (orig && !window._cmHooked) {
|
||
window._cmHooked = true;
|
||
window.showPage = function(pid) {
|
||
orig(pid);
|
||
if (pid === 'pageCoverage' && !window._cmLoaded) {
|
||
window._cmLoaded = true;
|
||
loadCoverageMatrix();
|
||
}
|
||
};
|
||
}
|
||
});
|
||
|
||
// === Document Viewer Modal ===
|
||
async function showDokViewerModal(docId) {
|
||
try {
|
||
const res = await fetch('/sport/api/v2/dokumenti/' + docId).then(r=>r.json());
|
||
const doc = res.data || res;
|
||
if (!doc) { alert('Dokument nije pronađen'); return; }
|
||
|
||
// Build modal
|
||
let bd = document.querySelector('.ri-modal');
|
||
if (bd) bd.remove();
|
||
bd = document.createElement('div');
|
||
bd.className = 'ri-modal';
|
||
bd.onclick = (e) => { if (e.target === bd) closeRiModal(); };
|
||
|
||
const formatList = [];
|
||
if (doc.url || doc.izvor_url || doc.pdf_url) formatList.push({label:'Originalni URL', url: doc.url || doc.izvor_url || doc.pdf_url, icon:iconExternal()});
|
||
if (doc.fname && doc.fname.endsWith('.pdf')) formatList.push({label:'PDF lokalno', url: '/sport/_files/' + doc.fname, icon:iconDownload()});
|
||
formatList.push({label:'Parsirani tekst', url: '/sport/api/v2/dokumenti/' + docId + '/text', icon:iconFile()});
|
||
|
||
bd.innerHTML = `
|
||
<div class="ri-modal-box" style="max-width:1200px;width:96%">
|
||
<div class="ri-modal-h">
|
||
<div style="flex:1">
|
||
<div style="font-weight:600;font-size:16px">${doc.title || doc.fname || 'Dokument #' + docId}</div>
|
||
<div class="muted" style="font-size:11px;margin-top:3px">
|
||
${doc.vrsta || ''} ${doc.organizacija ? '· ' + doc.organizacija : ''} ${doc.godina ? '· ' + doc.godina : ''}
|
||
${doc.sadrzaj ? '· ' + (doc.sadrzaj.length).toLocaleString('hr-HR') + ' znakova' : ''}
|
||
</div>
|
||
</div>
|
||
<button class="ri-icon-btn" onclick="closeRiModal()">${iconX()}</button>
|
||
</div>
|
||
<div class="ri-modal-body" style="padding:18px">
|
||
<div style="display:flex;gap:10px;margin-bottom:18px;flex-wrap:wrap">
|
||
${formatList.map(f => `<a href="${f.url}" target="_blank" class="ri-icon-btn" style="text-decoration:none;color:var(--text);padding:8px 14px;border:1px solid var(--border-1);border-radius:6px;display:inline-flex;align-items:center;gap:6px">
|
||
${f.icon} ${f.label}
|
||
</a>`).join('')}
|
||
</div>
|
||
${doc.kratak_opis ? `<div style="padding:14px;background:var(--bg-2);border-radius:8px;margin-bottom:18px">${doc.kratak_opis}</div>` : ''}
|
||
${doc.sadrzaj ? `<div style="font-family:monospace;font-size:12px;background:var(--bg-2);padding:14px;border-radius:8px;max-height:60vh;overflow-y:auto;white-space:pre-wrap;line-height:1.5">${doc.sadrzaj.substring(0,30000)}${doc.sadrzaj.length > 30000 ? '\n\n... (skraćeno, ' + (doc.sadrzaj.length - 30000).toLocaleString('hr-HR') + ' znakova više)' : ''}</div>` : '<div class="muted">Nema parsiranog teksta.</div>'}
|
||
</div>
|
||
</div>`;
|
||
document.body.appendChild(bd);
|
||
} catch(e) {
|
||
alert('Greška: ' + e);
|
||
}
|
||
}
|
||
|
||
// === Document list modal (filtered) ===
|
||
async function showDokListModal(filter, title) {
|
||
try {
|
||
let url = '/sport/api/v2/dokumenti?limit=200';
|
||
if (filter && filter.vrsta) url += '&vrsta=' + encodeURIComponent(filter.vrsta);
|
||
if (filter && filter.sport) url += '&sport=' + encodeURIComponent(filter.sport);
|
||
const res = await fetch(url).then(r=>r.json());
|
||
const docs = (res.data || res || []);
|
||
|
||
let bd = document.querySelector('.ri-modal');
|
||
if (bd) bd.remove();
|
||
bd = document.createElement('div');
|
||
bd.className = 'ri-modal';
|
||
bd.onclick = (e) => { if (e.target === bd) closeRiModal(); };
|
||
|
||
bd.innerHTML = `
|
||
<div class="ri-modal-box" style="max-width:1200px;width:96%">
|
||
<div class="ri-modal-h">
|
||
<div style="flex:1">${title || 'Dokumenti'} <span class="muted" style="font-size:12px">(${docs.length})</span></div>
|
||
<button class="ri-icon-btn" onclick="closeRiModal()">${iconX()}</button>
|
||
</div>
|
||
<div class="ri-modal-body" style="padding:0;max-height:75vh;overflow-y:auto">
|
||
<table class="ri-tbl ri-sortable" style="margin:0">
|
||
<thead><tr><th>Naslov</th><th>Vrsta</th><th>Org.</th><th>Godina</th><th style="text-align:right">Veličina</th><th style="text-align:right">Akcije</th></tr></thead>
|
||
<tbody>
|
||
${docs.map(d => `<tr style="cursor:pointer" onclick="showDokViewerModal(${d.id})">
|
||
<td><b>${d.title || d.fname || ('#'+d.id)}</b></td>
|
||
<td><span class="badge">${d.vrsta || '-'}</span></td>
|
||
<td>${d.organizacija || '-'}</td>
|
||
<td class="mono">${d.godina || '-'}</td>
|
||
<td class="mono" style="text-align:right">${d.sadrzaj_len ? d.sadrzaj_len.toLocaleString('hr-HR') : '-'}</td>
|
||
<td style="text-align:right;white-space:nowrap" onclick="event.stopPropagation()">
|
||
<button class="ri-icon-btn-sm" onclick="showDokViewerModal(${d.id})" title="Pregled">${iconEye()}</button>
|
||
${(d.url||d.izvor_url||d.pdf_url) ? `<a href="${d.url||d.izvor_url||d.pdf_url}" target="_blank" class="ri-icon-btn-sm" title="Originalni URL">${iconExternal()}</a>` : ''}
|
||
<a href="/sport/api/v2/dokumenti/${d.id}/text" target="_blank" class="ri-icon-btn-sm" title="Parsirani tekst">${iconFile()}</a>
|
||
</td>
|
||
</tr>`).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>`;
|
||
document.body.appendChild(bd);
|
||
autoAttachSort();
|
||
} catch(e) {
|
||
alert('Greška: ' + e);
|
||
}
|
||
}
|
||
|
||
|
||
// === AUDIT HELPERS ===
|
||
function auditRouteForTable(tabela) {
|
||
// 'klubovi (HBS)' / 'klubovi (HNS)' / 'clanovi' / 'natjecanja' / 'dokumenti' / 'utakmice_log'
|
||
const t = (tabela || '').toLowerCase();
|
||
if (t.startsWith('klubovi')) return "goto('klubovi')";
|
||
if (t.startsWith('clan')) return "goto('clanovi')";
|
||
if (t.startsWith('clan_nagrada')) return "goto('kategorije')";
|
||
if (t.startsWith('clan_sezona')) return "goto('kategorije')";
|
||
if (t.startsWith('natjecanja_tablice')) return "goto('natjecanja')";
|
||
if (t.startsWith('natjecanja')) return "goto('natjecanja')";
|
||
if (t.startsWith('dokumenti')) return "goto('dokumenti')";
|
||
if (t.startsWith('utakmice')) return "goto('natjecanja')";
|
||
return "goto('dashboard')";
|
||
}
|
||
|
||
function sourceRoute(scope, source) {
|
||
// Open filtered list of items by source
|
||
const src = encodeURIComponent(source || '');
|
||
if (scope === 'klubovi') return `state.scrapeFilter='${src}';goto('klubovi')`;
|
||
if (scope === 'sportasi') return `state.sourceFilter='${src}';goto('sportasi')`;
|
||
if (scope === 'natjecanja') return `state.natjecanjeSource='${src}';goto('natjecanja')`;
|
||
if (scope === 'dokumenti') return `showDokListModal({vrsta:'${src}'},'Dokumenti: ${src}')`;
|
||
return `goto('${scope}')`;
|
||
}
|
||
|
||
// === Card/Table view toggle helper ===
|
||
window.viewModes = window.viewModes || {};
|
||
|
||
function viewToggle(scope, currentMode) {
|
||
window.viewModes[scope] = currentMode === 'card' ? 'table' : 'card';
|
||
// Re-render current page
|
||
if (typeof render === 'function') render();
|
||
}
|
||
|
||
function viewToggleHTML(scope, defaultMode) {
|
||
const current = window.viewModes[scope] || defaultMode || 'table';
|
||
return `<div class="ri-vtoggle" style="display:inline-flex;background:var(--bg-2);border:1px solid var(--border-1);border-radius:6px;overflow:hidden;margin-left:auto">
|
||
<button class="ri-vtoggle-btn ${current==='table'?'active':''}" onclick="window.viewModes['${scope}']='table';if(typeof render==='function')render()" title="Tablica" style="padding:5px 10px;background:${current==='table'?'var(--accent)':'transparent'};color:${current==='table'?'#fff':'var(--text)'};border:none;cursor:pointer">${iconTable()}</button>
|
||
<button class="ri-vtoggle-btn ${current==='card'?'active':''}" onclick="window.viewModes['${scope}']='card';if(typeof render==='function')render()" title="Kartice" style="padding:5px 10px;background:${current==='card'?'var(--accent)':'transparent'};color:${current==='card'?'#fff':'var(--text)'};border:none;cursor:pointer">${iconCards()}</button>
|
||
</div>`;
|
||
}
|
||
|
||
function iconTable() { return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/></svg>'; }
|
||
function iconCards() { return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>'; }
|
||
|
||
|
||
// === MISSING HELPER FUNCTIONS (added 2026-05-02) ===
|
||
function spAddNew() {
|
||
// Open simple new sportaš modal — user fills in essentials
|
||
const html = `
|
||
<div class="ri-modal" id="riModal" onclick="if(event.target===this)closeRiModal()">
|
||
<div class="ri-modal-box" style="max-width:680px">
|
||
<div class="ri-modal-h"><h3 style="margin:0">Novi sportaš</h3>
|
||
<button class="ri-icon-btn" onclick="closeRiModal()">${iconX()}</button></div>
|
||
<div class="ri-modal-body" style="padding:18px;overflow-y:auto;max-height:75vh">
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px">
|
||
<div><label class="muted">Ime *</label><input id="spIme" class="inp" required></div>
|
||
<div><label class="muted">Prezime *</label><input id="spPrezime" class="inp" required></div>
|
||
<div><label class="muted">Spol</label>
|
||
<select id="spSpol" class="inp"><option value="">(nepoznat)</option><option value="M">M</option><option value="Ž">Ž</option></select></div>
|
||
<div><label class="muted">Sport</label>
|
||
<input id="spSport" class="inp" placeholder="nogomet, košarka..."></div>
|
||
<div><label class="muted">Datum rođenja</label><input id="spDob" type="date" class="inp"></div>
|
||
<div><label class="muted">Mjesto rođenja</label><input id="spMjesto" class="inp"></div>
|
||
<div><label class="muted">Klub</label>
|
||
<select id="spKlub" class="inp"><option value="">(nije izabran)</option></select></div>
|
||
<div><label class="muted">Uloga</label>
|
||
<select id="spUloga" class="inp">
|
||
<option value="igrac">igrač</option><option value="trener">trener</option>
|
||
<option value="kondicioni_trener">kondicioni trener</option>
|
||
<option value="predsjednik">predsjednik</option><option value="tajnik">tajnik</option>
|
||
<option value="direktor">direktor</option><option value="fizioterapeut">fizioterapeut</option>
|
||
<option value="lijecnik">liječnik</option><option value="sudac">sudac</option>
|
||
<option value="ostalo">ostalo</option>
|
||
</select></div>
|
||
<div style="grid-column:1/-1"><label class="muted">Slika (URL ili upload)</label>
|
||
<input id="spSlikaUrl" class="inp" placeholder="https://..."></div>
|
||
<div style="grid-column:1/-1"><label class="muted">Source URL (obavezno za datum_rodjenja!)</label>
|
||
<input id="spSourceUrl" class="inp" placeholder="https://..."></div>
|
||
<div style="grid-column:1/-1"><label class="muted">Napomena</label>
|
||
<textarea id="spNapomena" class="inp" rows="2"></textarea></div>
|
||
</div>
|
||
<div style="display:flex;justify-content:flex-end;gap:8px">
|
||
<button class="btn" onclick="closeRiModal()">Odustani</button>
|
||
<button class="btn primary" onclick="spSubmitNew()">Spremi</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
const div = document.createElement('div'); div.innerHTML = html;
|
||
document.body.appendChild(div.firstChild);
|
||
// Load klubovi for dropdown
|
||
fetch('/sport/api/v2/klubovi/sa-clanstvom?limit=500').then(r=>r.json()).then(d => {
|
||
const sel = document.getElementById('spKlub');
|
||
if (!sel) return;
|
||
(d.data || d.results || d || []).forEach(k => {
|
||
const opt = document.createElement('option');
|
||
opt.value = k.id; opt.textContent = k.naziv + (k.sport ? ' ('+k.sport+')' : '');
|
||
sel.appendChild(opt);
|
||
});
|
||
}).catch(()=>{});
|
||
}
|
||
|
||
async function spSubmitNew() {
|
||
const data = {
|
||
ime: document.getElementById('spIme').value.trim(),
|
||
prezime: document.getElementById('spPrezime').value.trim(),
|
||
spol: document.getElementById('spSpol').value || null,
|
||
sport: document.getElementById('spSport').value.trim() || null,
|
||
datum_rodjenja: document.getElementById('spDob').value || null,
|
||
mjesto_rodjenja: document.getElementById('spMjesto').value.trim() || null,
|
||
klub_id: document.getElementById('spKlub').value || null,
|
||
uloga: document.getElementById('spUloga').value,
|
||
slika_url: document.getElementById('spSlikaUrl').value.trim() || null,
|
||
source_url: document.getElementById('spSourceUrl').value.trim() || null,
|
||
napomena: document.getElementById('spNapomena').value.trim() || null
|
||
};
|
||
if (!data.ime || !data.prezime) { alert('Ime i prezime su obavezni'); return; }
|
||
if (data.datum_rodjenja && !data.source_url) {
|
||
alert('Datum rođenja zahtijeva source_url (DB policy).'); return;
|
||
}
|
||
try {
|
||
const res = await fetch('/sport/api/v2/sportas/create', {
|
||
method: 'POST',
|
||
headers: {'Content-Type':'application/json', 'Authorization':'Bearer '+(localStorage.getItem('rinet_v2_token')||'')},
|
||
body: JSON.stringify(data)
|
||
});
|
||
if (!res.ok) throw new Error('API ' + res.status + ': ' + await res.text());
|
||
const r = await res.json();
|
||
closeRiModal();
|
||
alert('Sportaš #' + (r.id || r.data?.id || '?') + ' kreiran');
|
||
if (typeof render === 'function') render();
|
||
} catch(e) { alert('Greška: ' + e.message); }
|
||
}
|
||
|
||
// Close modal helper (alias if not defined)
|
||
if (typeof closeRiModal !== 'function') {
|
||
window.closeRiModal = function(){
|
||
const m = document.getElementById('riModal'); if (m) m.remove();
|
||
};
|
||
}
|
||
|
||
|
||
// === GOOGLE AI ENRICHMENT POPUP ===
|
||
async function showEnrichModal(entityType, entityId, entityName, query) {
|
||
// entityType: 'klub' | 'manifestacija' | 'sportas' | 'objekt' | 'osoba' etc.
|
||
// entityName/query: natural language search query
|
||
let bd = document.querySelector('.ri-modal');
|
||
if (bd) bd.remove();
|
||
bd = document.createElement('div');
|
||
bd.className = 'ri-modal';
|
||
bd.id = 'riModal';
|
||
bd.onclick = (e) => { if (e.target === bd) closeRiModal(); };
|
||
|
||
bd.innerHTML = `
|
||
<div class="ri-modal-box" style="max-width:900px">
|
||
<div class="ri-modal-h">
|
||
<div style="flex:1">
|
||
<div style="font-weight:600">${entityName}</div>
|
||
<div class="muted" style="font-size:11px">${entityType} · AI obogaćeno</div>
|
||
</div>
|
||
<button class="ri-icon-btn" onclick="closeRiModal()">${iconX()}</button>
|
||
</div>
|
||
<div class="ri-modal-body" id="enrichBody" style="padding:20px">
|
||
<div style="text-align:center;padding:30px">
|
||
<div class="muted">⏳ Pretražujem internet i obogaćujem podatke o "${entityName}"...</div>
|
||
<div style="margin-top:14px;font-size:11px;color:var(--text3)">Ovo može potrajati 5-15 sekundi</div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
document.body.appendChild(bd);
|
||
|
||
try {
|
||
const res = await fetch('/sport/api/v2/enrich/google-ai', {
|
||
method: 'POST',
|
||
headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({entity_type: entityType, entity_id: entityId, query: query || entityName})
|
||
});
|
||
if (!res.ok) throw new Error('API ' + res.status);
|
||
const data = await res.json();
|
||
|
||
const body = document.getElementById('enrichBody');
|
||
if (!body) return;
|
||
|
||
let h = '';
|
||
if (data.summary) {
|
||
h += `<div style="background:var(--bg-2);padding:16px;border-radius:8px;margin-bottom:14px;line-height:1.6">${data.summary.replace(/\n/g,'<br>')}</div>`;
|
||
}
|
||
if (data.facts && data.facts.length) {
|
||
h += '<div style="margin-bottom:14px"><b style="color:var(--accent)">Ključne činjenice:</b><ul style="margin-top:8px">';
|
||
data.facts.forEach(f => h += `<li style="margin-bottom:6px">${f}</li>`);
|
||
h += '</ul></div>';
|
||
}
|
||
if (data.sources && data.sources.length) {
|
||
h += '<div style="border-top:1px solid var(--border-1);padding-top:14px;margin-top:14px"><b style="color:var(--text3);font-size:11px">IZVORI:</b><div style="margin-top:8px;display:flex;flex-direction:column;gap:6px">';
|
||
data.sources.forEach(s => {
|
||
h += `<a href="${s.url}" target="_blank" class="muted" style="font-size:12px;text-decoration:none;color:var(--accent);display:flex;align-items:center;gap:6px">${iconExternal()} ${s.title || s.url}</a>`;
|
||
});
|
||
h += '</div></div>';
|
||
}
|
||
if (data.saved_to_db) {
|
||
h += '<div style="margin-top:14px;padding:8px 12px;background:rgba(56,180,116,0.15);border-left:3px solid #38b474;font-size:12px;color:#38b474">✓ Spremljeno u bazu znanja (RAG embeddings)</div>';
|
||
}
|
||
if (data.google_search_url) {
|
||
h += `<div style="margin-top:14px;text-align:right"><a href="${data.google_search_url}" target="_blank" class="btn" style="text-decoration:none;display:inline-flex;align-items:center;gap:6px">${iconExternal()} Google search</a></div>`;
|
||
}
|
||
|
||
body.innerHTML = h || '<div class="muted">Nema podataka.</div>';
|
||
} catch(e) {
|
||
const body = document.getElementById('enrichBody');
|
||
if (body) body.innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>';
|
||
}
|
||
}
|
||
|
||
// Klik handler za "obogaćivanje" - kad nešto nema podatke
|
||
function enrichClick(type, id, name) {
|
||
showEnrichModal(type, id, name, name);
|
||
}
|
||
|
||
|
||
// Helper - shown at top of search results to enrich
|
||
function searchEnrichBtn(query) {
|
||
return `<button class="btn primary" onclick="showEnrichModal('search', null, ${JSON.stringify(query)}, ${JSON.stringify(query)})" style="display:flex;align-items:center;gap:6px">
|
||
${iconExternal()} AI obogati pretragu (Internet + LLM)
|
||
</button>`;
|
||
}
|
||
|
||
// Helper - shown on every entity that lacks data
|
||
function entityEnrichBtn(type, id, name) {
|
||
return `<button class="ri-icon-btn-sm" onclick="showEnrichModal('${type}', ${id || 'null'}, ${JSON.stringify(name)}, ${JSON.stringify(name)})" title="AI obogati podatke">
|
||
🔍 ${iconExternal()}
|
||
</button>`;
|
||
}
|
||
|
||
|
||
// Force-fresh reload helper (Ctrl+R doesn't always invalidate inline JS)
|
||
window.forceReload = function() {
|
||
const url = new URL(window.location.href);
|
||
url.searchParams.set('_v', Date.now());
|
||
window.location.href = url.toString();
|
||
};
|
||
|
||
|
||
// === KLUB WEB ENRICH HELPER ===
|
||
async function klubWebEnrich(klubId, klubNaziv) {
|
||
if (!confirm('Pokrenuti AI obogaćivanje za "' + klubNaziv + '" iz njihove web stranice?\n\nTo će dohvatiti uprava + stručni stožer + igrače sa klubske web stranice i upisati ih u bazu sa odgovarajućim ulogama.')) return;
|
||
|
||
let bd = document.querySelector('.ri-modal');
|
||
if (bd) bd.remove();
|
||
bd = document.createElement('div');
|
||
bd.className = 'ri-modal';
|
||
bd.id = 'riModal';
|
||
bd.onclick = (e) => { if (e.target === bd) closeRiModal(); };
|
||
bd.innerHTML = `
|
||
<div class="ri-modal-box" style="max-width:600px">
|
||
<div class="ri-modal-h"><div style="flex:1"><b>AI Obogaćivanje kluba</b><br><span class="muted" style="font-size:11px">${klubNaziv}</span></div>
|
||
<button class="ri-icon-btn" onclick="closeRiModal()">${iconX()}</button></div>
|
||
<div class="ri-modal-body" id="enrichBody" style="padding:24px;text-align:center">
|
||
<div class="muted">⏳ Dohvaćam podatke s web stranice...<br><span style="font-size:11px">Može potrajati 30-60 sekundi</span></div>
|
||
</div>
|
||
</div>`;
|
||
document.body.appendChild(bd);
|
||
|
||
try {
|
||
const res = await fetch('/sport/api/v2/enrich/klub-web', {
|
||
method: 'POST',
|
||
headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({klub_id: klubId})
|
||
});
|
||
const data = await res.json();
|
||
const body = document.getElementById('enrichBody');
|
||
if (!body) return;
|
||
|
||
if (data.error) {
|
||
body.innerHTML = '<div class="banner crit">' + data.error + '</div><div class="muted" style="font-size:11px;margin-top:8px">Možda klub nema web URL u bazi. Edit klub i dodaj web URL.</div>';
|
||
return;
|
||
}
|
||
|
||
let h = '<div style="text-align:left">';
|
||
h += '<div style="background:rgba(56,180,116,0.15);padding:14px;border-left:3px solid #38b474;border-radius:6px;margin-bottom:14px">';
|
||
h += '<div><b>✓ Obogaćivanje uspješno</b></div>';
|
||
h += '<div style="margin-top:8px">' + (data.people_count || 0) + ' osoba prepoznatih</div>';
|
||
h += '<div>Novih: <b>' + (data.inserted||0) + '</b></div>';
|
||
h += '<div>Ažuriranih: <b>' + (data.updated||0) + '</b></div>';
|
||
if (data.skipped) h += '<div>Preskočeno: ' + data.skipped + '</div>';
|
||
h += '</div>';
|
||
|
||
if (data.by_uloga) {
|
||
h += '<div style="margin-bottom:14px"><b>Po ulogama:</b><div style="margin-top:6px">';
|
||
Object.entries(data.by_uloga).forEach(([u, c]) => {
|
||
h += '<span class="badge" style="margin:2px">' + u + ': <b>' + c + '</b></span>';
|
||
});
|
||
h += '</div></div>';
|
||
}
|
||
|
||
if (data.fetched_urls) {
|
||
h += '<div style="border-top:1px solid var(--border-1);padding-top:12px"><b style="font-size:11px;color:var(--text3)">IZVORI:</b>';
|
||
data.fetched_urls.forEach(u => {
|
||
h += '<div style="margin-top:4px"><a href="' + u + '" target="_blank" style="font-size:11px;color:var(--accent);text-decoration:none">' + iconExternal() + ' ' + u + '</a></div>';
|
||
});
|
||
h += '</div>';
|
||
}
|
||
|
||
h += '<div style="margin-top:18px;text-align:right"><button class="btn primary" onclick="closeRiModal();render()">OK · Osvježi</button></div>';
|
||
h += '</div>';
|
||
body.innerHTML = h;
|
||
} catch(e) {
|
||
const body = document.getElementById('enrichBody');
|
||
if (body) body.innerHTML = '<div class="banner crit">Greška: ' + e.message + '</div>';
|
||
}
|
||
}
|
||
|
||
|
||
// Auto-inject klubWebEnrich button on klub roster page
|
||
(function() {
|
||
const origRender = window.render;
|
||
if (origRender && !window._klubBtnHook) {
|
||
window._klubBtnHook = true;
|
||
const obs = new MutationObserver(() => {
|
||
if (window.state && window.state.page === 'klubRoster' && window.state.klub_id) {
|
||
const breadcrumb = document.querySelector('.breadcrumbs') || document.querySelector('h1');
|
||
if (breadcrumb && !document.getElementById('klubEnrichBtn')) {
|
||
const btn = document.createElement('button');
|
||
btn.id = 'klubEnrichBtn';
|
||
btn.className = 'btn primary';
|
||
btn.style.cssText = 'margin-left:14px;display:inline-flex;align-items:center;gap:6px;font-size:12px;padding:6px 12px';
|
||
btn.innerHTML = '🔄 Obogati iz weba';
|
||
btn.onclick = () => {
|
||
const naziv = (document.querySelector('h1') || {}).textContent || 'klub';
|
||
klubWebEnrich(state.klub_id, naziv);
|
||
};
|
||
(breadcrumb.parentNode || breadcrumb).appendChild(btn);
|
||
}
|
||
}
|
||
});
|
||
obs.observe(document.getElementById('content') || document.body, {childList:true, subtree:true});
|
||
}
|
||
})();
|
||
|
||
|
||
// Delegated handler for chat enrich buttons (avoid inline onclick escape issues)
|
||
document.addEventListener('click', function(e) {
|
||
const btn = e.target.closest('.chat-enrich-btn');
|
||
if (btn) {
|
||
const q = btn.getAttribute('data-q') || '';
|
||
if (q && typeof showEnrichModal === 'function') {
|
||
showEnrichModal('search', null, q, q);
|
||
}
|
||
}
|
||
});
|
||
|
||
|
||
// === KLUB PICKER WITH AUTOCOMPLETE ===
|
||
function attachKlubPicker(inputId, onSelectCallback) {
|
||
const inp = document.getElementById(inputId);
|
||
if (!inp) return;
|
||
|
||
// Wrap input in container if not already
|
||
let wrap = inp.closest('.klub-picker-wrap');
|
||
if (!wrap) {
|
||
wrap = document.createElement('div');
|
||
wrap.className = 'klub-picker-wrap';
|
||
wrap.style.cssText = 'position:relative;display:inline-block;width:100%;max-width:400px';
|
||
inp.parentNode.insertBefore(wrap, inp);
|
||
wrap.appendChild(inp);
|
||
}
|
||
|
||
// Suggest dropdown
|
||
let drop = wrap.querySelector('.klub-suggest');
|
||
if (!drop) {
|
||
drop = document.createElement('div');
|
||
drop.className = 'klub-suggest';
|
||
drop.style.cssText = 'display:none;position:absolute;top:100%;left:0;right:0;max-height:280px;overflow-y:auto;background:var(--bg-1);border:1px solid var(--border-1);border-radius:6px;z-index:100;margin-top:2px;box-shadow:0 4px 16px rgba(0,0,0,0.4)';
|
||
wrap.appendChild(drop);
|
||
}
|
||
|
||
inp.setAttribute('placeholder', 'Upiši ime kluba (npr. Zamet, Rijeka...)');
|
||
|
||
let timer = null;
|
||
inp.oninput = function() {
|
||
clearTimeout(timer);
|
||
const q = inp.value.trim();
|
||
if (!q || q.length < 2) { drop.style.display = 'none'; return; }
|
||
timer = setTimeout(async () => {
|
||
try {
|
||
const r = await fetch('/sport/api/v2/klubovi?q=' + encodeURIComponent(q) + '&limit=20');
|
||
const d = await r.json();
|
||
const items = d.data || d.results || d || [];
|
||
if (!items.length) { drop.style.display = 'none'; return; }
|
||
drop.innerHTML = items.map(k =>
|
||
`<div class="klub-suggest-item" data-id="${k.id}" data-naziv="${(k.naziv||'').replace(/"/g,'"')}"
|
||
style="padding:8px 12px;cursor:pointer;border-bottom:1px solid var(--border-2);font-size:13px">
|
||
<b>${k.naziv}</b> ${k.sport ? '<span class="muted" style="font-size:11px">· '+k.sport+'</span>':''}
|
||
${k.grad ? '<span class="muted" style="font-size:11px"> · '+k.grad+'</span>':''}
|
||
<span style="color:var(--text3);font-size:10px;float:right">#${k.id}</span>
|
||
</div>`).join('');
|
||
drop.style.display = 'block';
|
||
drop.querySelectorAll('.klub-suggest-item').forEach(el => {
|
||
el.onclick = () => {
|
||
const kid = parseInt(el.getAttribute('data-id'));
|
||
const naziv = el.getAttribute('data-naziv');
|
||
inp.value = naziv + ' (#' + kid + ')';
|
||
inp.setAttribute('data-klub-id', kid);
|
||
drop.style.display = 'none';
|
||
if (onSelectCallback) onSelectCallback(kid, naziv);
|
||
};
|
||
el.onmouseover = () => el.style.background = 'var(--bg-2)';
|
||
el.onmouseout = () => el.style.background = 'transparent';
|
||
});
|
||
} catch(e) { drop.style.display = 'none'; }
|
||
}, 200);
|
||
};
|
||
|
||
inp.onblur = () => setTimeout(() => { drop.style.display = 'none'; }, 200);
|
||
}
|
||
|
||
// Helper: extract klub_id from picker input
|
||
function getPickerKlubId(inputId) {
|
||
const inp = document.getElementById(inputId);
|
||
if (!inp) return null;
|
||
// Check data attribute first (was selected)
|
||
const did = inp.getAttribute('data-klub-id');
|
||
if (did) return parseInt(did);
|
||
// Otherwise try parse "Klub Name (#123)" pattern
|
||
const m = (inp.value || '').match(/#(\d+)/);
|
||
if (m) return parseInt(m[1]);
|
||
return null;
|
||
}
|
||
|
||
|
||
// Auto-sort klub roster table by uloga group
|
||
(function() {
|
||
if (window._klubRosterSortHook) return;
|
||
window._klubRosterSortHook = true;
|
||
|
||
const ULOGA_ORDER = {
|
||
'predsjednik': 1, 'dopredsjednik': 2, 'tajnik': 3, 'direktor': 4,
|
||
'član uprave': 5, 'član nadzornog odbora': 6,
|
||
'trener': 10, 'pomocni_trener': 11, 'trener_vratara': 12,
|
||
'kondicioni_trener': 13, 'fizioterapeut': 14, 'lijecnik': 15,
|
||
'team_manager': 16, 'analiticar': 17, 'video_analiticar': 18,
|
||
'igrac': 50, 'sportaš': 51, 'sportas': 51,
|
||
'sudac': 60, 'ostalo': 90, '': 99
|
||
};
|
||
|
||
// No client-side reorder needed - depends on data structure
|
||
})();
|
||
|
||
|
||
// === OBRASCI SPORTSKIH SAVEZA (RSS, ZSP PGŽ, HOO, Lokalni savezi) ===
|
||
window.OBRASCI_KATALOG = [
|
||
// RSS Rijeka (Riječki sportski savez)
|
||
{ id:'rss-prijava-godisnjak', naziv:'Prijava za Godišnjak ZS PGŽ', savez:'RSS Rijeka / ZS PGŽ', kategorija:'Godisnji', polja:['Naziv kluba','Sport','OIB','Predsjednik','Tajnik','Adresa','Email','Web','Broj članova M','Broj članova Ž','Broj kategoriziranih','Broj reprezentativaca','Trofeji 2025','Najveći uspjeh'] },
|
||
{ id:'rss-zahtjev-program', naziv:'Zahtjev za sufinanciranje programa sporta', savez:'RSS Rijeka', kategorija:'Financiranje', polja:['Klub','OIB','Naziv programa','Iznos zahtjeva','Razdoblje od','Razdoblje do','Broj polaznika','Cilj programa','Voditelj','Telefon'] },
|
||
{ id:'rss-natjecanje', naziv:'Prijava manifestacije / natjecanja', savez:'RSS Rijeka', kategorija:'Manifestacija', polja:['Klub organizator','Naziv manifestacije','Mjesto','Datum','Razina (lokalna/županijska/državna/međunarodna)','Broj sudionika očekivanih','Sport','Voditelj','Mobitel','Email'] },
|
||
// ZS PGŽ
|
||
{ id:'zs-pgz-godisnji-izvjestaj', naziv:'Godišnji izvještaj kluba ZS PGŽ', savez:'ZS PGŽ', kategorija:'Godisnji', polja:['Klub','OIB','Sport','Predsjednik','Broj članova ukupno','Broj M','Broj Ž','Broj kategoriziranih HOO','Broj reprezentativaca','Najveći uspjesi 2025','Plan 2026'] },
|
||
{ id:'zs-pgz-najuspjesniji', naziv:'Prijedlog za nagradu ZS PGŽ "Najuspješniji"', savez:'ZS PGŽ', kategorija:'Nagrade', polja:['Kategorija (sportaš godine, ekipa godine, trener, životno djelo)','Predloženik','Klub','Sport','Obrazloženje','Postignuće 2025'] },
|
||
{ id:'zs-pgz-stipendija', naziv:'Zahtjev za sportsku stipendiju PGŽ', savez:'PGŽ', kategorija:'Stipendije', polja:['Sportaš ime i prezime','OIB','Datum rođenja','Klub','Sport','Kategorija HOO','Reprezentacija','Najveći uspjesi','Iznos zahtjeva','IBAN','Suglasnost roditelja (PDF)'] },
|
||
// PGŽ proračun
|
||
{ id:'pgz-prijava-natjecaj', naziv:'Prijava na javni natječaj PGŽ za sport', savez:'PGŽ', kategorija:'Financiranje', polja:['Naziv kluba','OIB','Predsjednik','Adresa','Iznos zahtjeva','Aktivnost','Razdoblje','Voditelj','IBAN','Banka'] },
|
||
// Putni nalog (klubsko interno)
|
||
{ id:'klub-putni-nalog', naziv:'Putni nalog za klupskog djelatnika', savez:'Interno klub', kategorija:'Operativa', polja:['Klub','Ime i prezime','Funkcija','Vrsta vozila','Polazište','Odredište','Datum polaska','Datum povratka','Svrha putovanja','Iznos predujma EUR'] },
|
||
// Liječnička potvrda
|
||
{ id:'lijecnicki-pregled', naziv:'Zahtjev za sportsko-liječnički pregled (ZZJZ PGŽ)', savez:'ZZJZ PGŽ', kategorija:'Medicina', polja:['Sportaš','OIB','Klub','Sport','Datum rođenja','Vrsta pregleda (osnovni/specijalistički)','Datum željen','Telefon','Email'] },
|
||
// Kategorizacija HOO
|
||
{ id:'hoo-kategorizacija', naziv:'Prijava za HOO kategorizaciju sportaša', savez:'HOO (preko saveza)', kategorija:'Kategorizacija', polja:['Sportaš','OIB','Datum rođenja','Klub','Sport','Disciplina','Prijedlog kategorije (I/II/III)','Postignuća (godina rezultat)','Reprezentacija (Da/Ne)'] },
|
||
// Klub osnivanje
|
||
{ id:'osnivanje-kluba', naziv:'Zahtjev za upis kluba u registar saveza', savez:'Lokalni savez', kategorija:'Klub', polja:['Naziv kluba','Skraćeni naziv','OIB','Adresa','Predsjednik','OIB predsjednika','Tajnik','Sport','Datum osnivačke skupštine','Broj članova osnivača'] }
|
||
];
|
||
|
||
async function pageObrasci() {
|
||
// Deprecated - redirect to backend-driven forms page
|
||
if (typeof pageForms === 'function') return pageForms();
|
||
state.page = 'forms';
|
||
setTopbar('Obrasci', 'Backend templates');
|
||
}
|
||
|
||
function openObrazac(id) {
|
||
const obr = OBRASCI_KATALOG.find(o => o.id === id);
|
||
if (!obr) return;
|
||
|
||
let bd = document.querySelector('.ri-modal');
|
||
if (bd) bd.remove();
|
||
bd = document.createElement('div');
|
||
bd.className = 'ri-modal';
|
||
bd.id = 'riModal';
|
||
bd.onclick = (e) => { if (e.target === bd) closeRiModal(); };
|
||
|
||
let formHtml = '<div class="ri-modal-box" style="max-width:720px">';
|
||
formHtml += `<div class="ri-modal-h"><div style="flex:1"><b>${obr.naziv}</b><br><span class="muted" style="font-size:11px">${obr.savez} · ${obr.kategorija}</span></div>
|
||
<button class="ri-icon-btn" onclick="closeRiModal()">${iconX()}</button></div>`;
|
||
formHtml += '<div class="ri-modal-body" style="padding:20px;overflow-y:auto;max-height:75vh">';
|
||
formHtml += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px">';
|
||
obr.polja.forEach((p, i) => {
|
||
const isLong = p.toLowerCase().includes('obraz') || p.toLowerCase().includes('cilj') || p.toLowerCase().includes('uspj') || p.toLowerCase().includes('plan');
|
||
if (isLong) {
|
||
formHtml += `<div style="grid-column:1/-1"><label class="muted" style="font-size:11px">${p}</label><textarea id="obr_${i}" class="inp" rows="3" style="width:100%"></textarea></div>`;
|
||
} else {
|
||
formHtml += `<div><label class="muted" style="font-size:11px">${p}</label><input id="obr_${i}" class="inp" style="width:100%"></div>`;
|
||
}
|
||
});
|
||
formHtml += '</div>';
|
||
formHtml += '<div style="margin-top:18px;display:flex;justify-content:flex-end;gap:8px">';
|
||
formHtml += `<button class="btn" onclick="obrazacPrint('${obr.id}')">🖨 Print</button>`;
|
||
formHtml += `<button class="btn" onclick="obrazacSavePDF('${obr.id}')">📥 PDF</button>`;
|
||
formHtml += `<button class="btn primary" onclick="obrazacSubmit('${obr.id}')">📤 Pošalji ${obr.savez.split(' ')[0]}</button>`;
|
||
formHtml += '</div></div></div>';
|
||
bd.innerHTML = formHtml;
|
||
document.body.appendChild(bd);
|
||
}
|
||
|
||
async function obrazacSubmit(id) {
|
||
const obr = OBRASCI_KATALOG.find(o => o.id === id);
|
||
const data = {};
|
||
obr.polja.forEach((p, i) => {
|
||
data[p] = (document.getElementById('obr_'+i) || {}).value || '';
|
||
});
|
||
|
||
try {
|
||
const r = await fetch('/sport/api/v2/obrasci/submit', {
|
||
method: 'POST',
|
||
headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({obrazac_id: id, naziv: obr.naziv, savez: obr.savez, data: data})
|
||
});
|
||
if (!r.ok) throw new Error('API ' + r.status);
|
||
const res = await r.json();
|
||
alert('✓ Obrazac spremljen #' + (res.id || '?') + '\nPrimit će ga: ' + obr.savez);
|
||
closeRiModal();
|
||
} catch(e) {
|
||
alert('Greška: ' + e.message + '\n\nObrazac trenutno nije implementiran na backendu - generiram PDF lokalno.');
|
||
obrazacSavePDF(id);
|
||
}
|
||
}
|
||
|
||
function obrazacPrint(id) { window.print(); }
|
||
function obrazacSavePDF(id) {
|
||
// Use browser print to PDF
|
||
const obr = OBRASCI_KATALOG.find(o => o.id === id);
|
||
let html = '<html><head><title>' + obr.naziv + '</title><style>body{font-family:sans-serif;padding:30px}h1{color:#1e40af}label{display:block;margin-top:14px;font-weight:600}.val{padding:6px;border-bottom:1px solid #999;min-height:20px}</style></head><body>';
|
||
html += '<h1>' + obr.naziv + '</h1>';
|
||
html += '<p><b>Savez:</b> ' + obr.savez + '<br><b>Datum:</b> ' + new Date().toLocaleDateString('hr-HR') + '</p>';
|
||
obr.polja.forEach((p, i) => {
|
||
const v = (document.getElementById('obr_'+i) || {}).value || '';
|
||
html += '<label>' + p + ':</label><div class="val">' + (v.replace(/</g,'<') || '—') + '</div>';
|
||
});
|
||
html += '<p style="margin-top:40px"><b>Potpis:</b> ____________________ <b>Pečat:</b></p>';
|
||
html += '</body></html>';
|
||
const w = window.open('', '_blank');
|
||
w.document.write(html); w.document.close();
|
||
setTimeout(() => w.print(), 300);
|
||
}
|
||
|
||
|
||
// Auto-attach klub picker to all invoice klub inputs
|
||
(function() {
|
||
if (window._erpKlubPickerHook) return;
|
||
window._erpKlubPickerHook = true;
|
||
const obs = new MutationObserver(() => {
|
||
if (window.state && (state.page === 'invoices' || state.page === 'expenses')) {
|
||
// Find any input with klub keyword that's not already wrapped
|
||
document.querySelectorAll('input').forEach(inp => {
|
||
if (inp._klubPickerAttached) return;
|
||
const ph = (inp.placeholder || '').toLowerCase();
|
||
const id = (inp.id || '').toLowerCase();
|
||
const name = (inp.name || '').toLowerCase();
|
||
if ((ph.includes('klub') || id.includes('klub') || name.includes('klub'))
|
||
&& !inp.closest('.klub-picker-wrap')
|
||
&& inp.type !== 'hidden') {
|
||
inp._klubPickerAttached = true;
|
||
if (!inp.id) inp.id = 'klubInp_' + Math.random().toString(36).substr(2,8);
|
||
attachKlubPicker(inp.id);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
obs.observe(document.body, {childList:true, subtree:true});
|
||
})();
|
||
|
||
|
||
// Aggressively attach klub picker to invKlub input whenever it appears
|
||
(function() {
|
||
if (window._invKlubAttachHook) return;
|
||
window._invKlubAttachHook = true;
|
||
let lastSeen = null;
|
||
setInterval(() => {
|
||
const el = document.getElementById('invKlub');
|
||
if (el && el !== lastSeen && !el._klubPickerAttached) {
|
||
el._klubPickerAttached = true;
|
||
lastSeen = el;
|
||
if (typeof attachKlubPicker === 'function') {
|
||
attachKlubPicker('invKlub');
|
||
console.log('[ri] klub picker attached to invKlub');
|
||
}
|
||
}
|
||
}, 500);
|
||
})();
|
||
|
||
|
||
// ===== RNO =====
|
||
async function pageRno() {
|
||
document.getElementById('content').innerHTML = `<div class="page-h"><h2>Registar NPO</h2><p class="muted">1.505 sportskih organizacija PGZ iz Registra neprofitnih — s financijskim podacima u EUR.</p></div>
|
||
<div class="card" style="margin-bottom:12px;padding:10px 12px;background:var(--bg3)"><div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
||
<span style="font-size:11px;color:var(--muted)">Pretraga</span>
|
||
<input id="rnoSearch" class="i" type="text" placeholder="Naziv, OIB, mjesto..." style="flex:1;min-width:160px" oninput="loadRno()">
|
||
<span style="font-size:11px;color:var(--muted)">Status</span>
|
||
<select id="rnoStatus" class="i" onchange="loadRno()" style="min-width:100px"><option value="">Sve</option><option value="active">Aktivne</option><option value="inactive">Neaktivne</option></select>
|
||
<span style="font-size:11px;color:var(--muted)">Sortiraj</span>
|
||
<select id="rnoSort" class="i" onchange="loadRno()" style="min-width:110px"><option value="naziv">Naziv</option><option value="prihodi">Prihodi</option><option value="rashodi">Rashodi</option></select>
|
||
</div></div>
|
||
<div id="rnoList" class="loader">Ucitavanje...</div>`;
|
||
loadRno();
|
||
}
|
||
async function loadRno() {
|
||
const q=document.getElementById('rnoSearch')?.value||'', status=document.getElementById('rnoStatus')?.value||'', sort=document.getElementById('rnoSort')?.value||'naziv';
|
||
const data=await api('/api/v2/rno?'+new URLSearchParams({q,status,sort,limit:100}));
|
||
const el=document.getElementById('rnoList'); if(!el)return;
|
||
if(!data?.length){el.innerHTML='<p class="muted">Nema rezultata.</p>';return;}
|
||
const ef=n=>n?new Intl.NumberFormat('hr-HR',{style:'currency',currency:'EUR',maximumFractionDigits:0}).format(n<1000&&n>0?n:n/7.5345):'-';
|
||
el.innerHTML=`<div style="font-size:11px;color:var(--muted);margin-bottom:8px">${data.length} organizacija</div><div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:12px">
|
||
<thead><tr style="background:var(--bg3)"><th style="text-align:left;padding:7px 10px;font-size:10px;color:var(--muted)">NAZIV</th><th style="padding:7px 10px;font-size:10px;color:var(--muted)">MJESTO</th><th style="text-align:right;padding:7px 10px;font-size:10px;color:var(--muted)">PRIHODI EUR</th><th style="text-align:right;padding:7px 10px;font-size:10px;color:var(--muted)">RASHODI EUR</th><th style="text-align:center;padding:7px 10px;font-size:10px;color:var(--muted)">AKT.</th></tr></thead>
|
||
<tbody>${data.map(r=>`<tr style="border-top:1px solid var(--border)"><td style="padding:6px 10px"><div style="color:var(--text);font-weight:500">${r.naziv}</div><div style="font-size:10px;color:var(--muted)">${r.oib||''}</div></td><td style="padding:6px 10px;font-size:11px;color:var(--text2)">${r.mjesto||'-'}</td><td style="padding:6px 10px;text-align:right;font-family:monospace;font-size:11px">${ef(r.prihodi)}</td><td style="padding:6px 10px;text-align:right;font-family:monospace;font-size:11px">${ef(r.rashodi)}</td><td style="padding:6px 10px;text-align:center;color:${r.aktivna?'#22c55e':'var(--muted)'}">${r.aktivna?'✓':'-'}</td></tr>`).join('')}</tbody>
|
||
</table></div>`;}
|
||
|
||
// ===== HNS =====
|
||
async function pageHns() {
|
||
document.getElementById('content').innerHTML=`<div class="page-h"><h2>HNS Natjecanja PGZ</h2><p class="muted">NS Rijeka i ZNS PGZ — sezone 2022-2026.</p></div>
|
||
<div class="card" style="margin-bottom:12px;padding:10px 12px;background:var(--bg3)"><div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
||
<span style="font-size:11px;color:var(--muted)">Sezona</span>
|
||
<select id="hnsSeason" class="i" onchange="loadHns()" style="min-width:110px"><option value="">Sve</option><option value="2025/2026" selected>2025/2026</option><option value="2024/2025">2024/2025</option><option value="2023/2024">2023/2024</option><option value="2022/2023">2022/2023</option></select>
|
||
<span style="font-size:11px;color:var(--muted)">Org.</span>
|
||
<select id="hnsOrg" class="i" onchange="loadHns()" style="min-width:120px"><option value="">Obje</option><option value="178180">NS Rijeka</option><option value="51">ZNS PGZ</option></select>
|
||
</div></div>
|
||
<div id="hnsList" class="loader">Ucitavanje...</div>`;
|
||
loadHns();
|
||
}
|
||
async function loadHns(){
|
||
const season=document.getElementById('hnsSeason')?.value||'', org=document.getElementById('hnsOrg')?.value||'';
|
||
const data=await api('/api/v2/hns-natjecanja?'+new URLSearchParams({season,org}));
|
||
const el=document.getElementById('hnsList'); if(!el)return;
|
||
if(!data?.length){el.innerHTML='<p class="muted">Nema natjecanja.</p>';return;}
|
||
const bySeason={};
|
||
data.forEach(r=>{if(!bySeason[r.sezona])bySeason[r.sezona]=[];bySeason[r.sezona].push(r);});
|
||
el.innerHTML=Object.entries(bySeason).sort((a,b)=>b[0].localeCompare(a[0])).map(([s,comps])=>`
|
||
<div style="margin-bottom:16px"><div style="font-size:10px;font-weight:700;color:var(--accent);margin-bottom:8px;text-transform:uppercase;letter-spacing:1px">Sezona ${s} - ${comps.length} natjecanja</div>
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:8px">
|
||
${comps.map(c=>`<a href="${c.url||'#'}" target="_blank" style="display:block;padding:10px 12px;background:var(--bg3);border:1px solid var(--border);border-radius:6px;text-decoration:none;font-size:12px;font-weight:500;color:var(--text);transition:border-color .15s" onmouseover="this.style.borderColor='var(--accent)'" onmouseout="this.style.borderColor='var(--border)'">${c.naziv}<span style="display:block;font-size:10px;color:var(--muted);margin-top:2px;font-weight:400">${c.org_id==178180?'NS Rijeka':'ZNS PGZ'}</span></a>`).join('')}
|
||
</div></div>`).join('');}
|
||
|
||
// ===== GODISNJACI =====
|
||
async function pageGodisnjaci(){
|
||
document.getElementById('content').innerHTML=`<div class="page-h"><h2>Godisnjaci ZSP PGZ — AI</h2><p class="muted">AI pretraga 19 godisnjaka (2006-2024). Pitaj na prirodnom jeziku.</p></div>
|
||
<div class="card" style="margin-bottom:12px;padding:10px 12px;background:var(--bg3)">
|
||
<div style="display:flex;gap:8px"><input id="godQ" class="i" type="text" placeholder="Npr: Koliko kosarkaskih klubova 2015?" style="flex:1"><button class="btn" onclick="searchGod()">Pretrazi</button></div>
|
||
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px" id="godChips"></div></div>
|
||
<div id="godLoader" style="display:none" class="loader">AI pretrazuje...</div>
|
||
<div id="godResult" style="display:none"><div id="godAns" class="card" style="margin-bottom:10px;background:var(--bg4)"></div><div id="godSrc"></div></div>`;
|
||
const chips=['Kosarka Rijeka','Proracun sport 2019','Planinarstvo PGZ','Atletika mladi','Vaterpolo'];
|
||
document.getElementById('godChips').innerHTML=chips.map(q=>`<span onclick="document.getElementById('godQ').value='${q}';searchGod()" style="cursor:pointer;padding:2px 10px;background:var(--bg3);border:1px solid var(--border);border-radius:12px;font-size:11px;color:var(--text2)">${q}</span>`).join('');
|
||
document.getElementById('godQ').addEventListener('keydown',e=>{if(e.key==='Enter')searchGod();});
|
||
}
|
||
async function searchGod(){
|
||
const q=document.getElementById('godQ').value.trim(); if(!q)return;
|
||
document.getElementById('godLoader').style.display='block';
|
||
document.getElementById('godResult').style.display='none';
|
||
try{
|
||
const res=await api('/api/v2/godisnjaci/search',{method:'POST',body:JSON.stringify({question:q})});
|
||
document.getElementById('godLoader').style.display='none';
|
||
document.getElementById('godResult').style.display='block';
|
||
document.getElementById('godAns').innerHTML=`<div style="font-size:10px;color:var(--accent);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px">AI Odgovor</div><div style="font-size:13px;line-height:1.6">${res.answer||'Nema odgovora.'}</div>`;
|
||
document.getElementById('godSrc').innerHTML=(res.sources||[]).map(s=>`<div class="card" style="margin-bottom:8px;font-size:12px"><div style="color:var(--accent);margin-bottom:4px">Godisnjak ${s.godina}</div><div style="color:var(--text2);line-height:1.5">${(s.text||'').slice(0,300)}...</div></div>`).join('');
|
||
}catch(e){document.getElementById('godLoader').style.display='none';document.getElementById('godResult').style.display='block';document.getElementById('godAns').innerHTML=`<span class="dim">Greska: ${e.message}</span>`;}}
|
||
|
||
|
||
// Browser back/forward button support
|
||
window.addEventListener('popstate', function() {
|
||
const h = window.location.hash.slice(1);
|
||
if (h && typeof window['page'+h[0].toUpperCase()+h.slice(1)] === 'function') {
|
||
window['page'+h[0].toUpperCase()+h.slice(1)]();
|
||
} else if (h) {
|
||
goto(h);
|
||
}
|
||
});
|
||
|
||
// ===== SPORTSKI OBJEKTI =====
|
||
async function pageObjekti() {
|
||
document.getElementById('content').innerHTML = `<div class="page-h"><h2>Sportski objekti PGZ</h2><p class="muted">106 sportskih objekata — dvorane, stadioni, bazeni, tereni. Klik otvara na karti.</p></div>
|
||
<div class="card" style="margin-bottom:12px;padding:10px 12px;background:var(--bg3)"><div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
||
<span style="font-size:11px;color:var(--muted)">Tip</span>
|
||
<select id="objTip" class="i" onchange="loadObjekti()" style="min-width:120px">
|
||
<option value="">Svi tipovi</option>
|
||
<option value="dvorana">Dvorana</option>
|
||
<option value="stadion">Stadion</option>
|
||
<option value="bazen">Bazen</option>
|
||
<option value="teren">Teren</option>
|
||
<option value="kompleks">Kompleks</option>
|
||
<option value="klizaliste">Klizaliste</option>
|
||
</select>
|
||
<span style="font-size:11px;color:var(--muted)">Grad</span>
|
||
<select id="objGrad" class="i" onchange="loadObjekti()" style="min-width:110px">
|
||
<option value="">Svi gradovi</option>
|
||
</select>
|
||
<input id="objQ" class="i" type="text" placeholder="Pretrazi..." style="flex:1;min-width:120px" oninput="loadObjekti()">
|
||
</div></div>
|
||
<div id="objList" class="loader">Ucitavanje...</div>`;
|
||
loadObjekti();
|
||
}
|
||
async function loadObjekti() {
|
||
const tip=document.getElementById('objTip')?.value||'', grad=document.getElementById('objGrad')?.value||'', q=document.getElementById('objQ')?.value||'';
|
||
const data = await api('/api/v2/sport/objekti?'+new URLSearchParams({tip,grad,q}));
|
||
const el=document.getElementById('objList'); if(!el)return;
|
||
|
||
// Populate grad dropdown on first load
|
||
if(!document.getElementById('objGrad').options.length || document.getElementById('objGrad').options.length===1) {
|
||
const grads=[...new Set((data||[]).map(r=>r.grad).filter(Boolean))].sort();
|
||
const gEl=document.getElementById('objGrad');
|
||
gEl.innerHTML='<option value="">Svi gradovi</option>'+grads.map(g=>`<option value="${g}">${g}</option>`).join('');
|
||
}
|
||
|
||
if(!data?.length){el.innerHTML='<p class="muted" style="padding:20px">Nema objekata za filtere.</p>';return;}
|
||
el.innerHTML=`<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px">
|
||
${data.filter(r=>!q||(r.naziv||'').toLowerCase().includes(q.toLowerCase())).map(r=>`
|
||
<div class="card" style="padding:12px 14px;background:var(--bg3)">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
|
||
<span style="font-size:12px;font-weight:600;color:var(--text)">${r.naziv||'—'}</span>
|
||
<span style="font-size:10px;padding:2px 6px;background:rgba(var(--accent-rgb,245,158,11),.15);color:var(--accent);border-radius:10px;text-transform:capitalize">${r.tip||'—'}</span>
|
||
</div>
|
||
<div style="font-size:11px;color:var(--text2);margin-bottom:4px">📍 ${r.adresa||r.grad||'—'}</div>
|
||
${r.sportovi?.length?`<div style="font-size:10px;color:var(--muted);margin-bottom:4px">⚽ ${r.sportovi.join(', ')}</div>`:''}
|
||
${r.kapacitet?`<div style="font-size:10px;color:var(--muted)">👥 ${r.kapacitet} mjesta · Izgradeno: ${r.izgradeno||'?'}</div>`:''}
|
||
${r.web?`<a href="${r.web}" target="_blank" style="font-size:10px;color:var(--accent);text-decoration:none">🔗 Web</a>`:''}
|
||
${r.lat&&r.lng?`<a href="https://maps.google.com/?q=${r.lat},${r.lng}" target="_blank" style="font-size:10px;color:var(--accent);margin-left:8px;text-decoration:none">🗺 Karta</a>`:''}
|
||
</div>`).join('')}
|
||
</div>`;}
|
||
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|