Files
pgz-sport/_backups/r3_cc5/app.html.cc5_links.1777933397
T
Damir Radulić f5c6570d47 CC2 R4 #2+#5: remove legacy unauth /api/admin/users — close 401 gap
The bare @app.get/post('/api/admin/users') decorators in pgz_sport_api.py
were registered before app.include_router(admin_users_router) and shadowed
the JWT-protected M2 routes, leaking user list to anyone.

Removed all three: GET /api/admin/users, POST /api/admin/users,
POST /api/admin/users/{uid}/toggle. The auth.admin_users router now owns
this prefix exclusively and gates every method with require_user.

Verified: no-auth → 401, invalid token → 401, valid Bearer → 200.
2026-05-05 00:44:50 +02:00

1215 lines
72 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="hr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>PGŽ SPORT — Operativna aplikacija</title>
<!--
app.html v1.0 — Round 3 M4
PGŽ Sport operational app — 4 role dashboards (PGŽ admin, savez admin, klub admin, sportaš)
Author: dradulic@outlook.com / damir@rinet.one — 2026-05-05
Same CSS variables / theme as sport2.html. Sidebar is collapsible (M3 logic).
-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
<style>
:root{
--pgz-blue:#003087; --pgz-blue2:#004CC4; --pgz-gold:#F4C430;
--bg0:#08090e; --bg1:#0d1021; --bg2:#111628; --bg3:#161d35; --bg4:#1c2542;
--rim:#1e2a50; --rim2:#283560;
--t0:#fff; --t1:#e2e6f0; --t2:#8a95b4; --t4:#4e5a7a;
--green:#00e88f; --red:#ff2d55; --amber:#f59e0b; --cyan:#00c8e8;
--font:'Inter',sans-serif; --mono:'JetBrains Mono',monospace;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{font-family:var(--font);background:var(--bg0);color:var(--t1);font-size:13px;overflow-x:hidden}
a{color:var(--cyan);text-decoration:none}
a:hover{color:var(--pgz-gold)}
button,input,select,textarea{font-family:inherit;font-size:inherit;outline:none}
::-webkit-scrollbar{width:8px;height:8px}
::-webkit-scrollbar-track{background:var(--bg1)}
::-webkit-scrollbar-thumb{background:var(--rim2);border-radius:4px}
::-webkit-scrollbar-thumb:hover{background:var(--pgz-blue2)}
/* ============ LAYOUT ============ */
.app{display:flex;min-height:100vh}
.sb{width:240px;background:linear-gradient(180deg,var(--bg1) 0%,var(--bg0) 100%);border-right:1px solid var(--rim);position:fixed;top:0;left:0;bottom:0;display:flex;flex-direction:column;z-index:10;transition:width .22s ease}
.sb-h{padding:18px 18px 14px;border-bottom:1px solid var(--rim);position:relative}
.sb-h .logo{font-weight:800;font-size:14px;color:var(--t0);letter-spacing:.5px;white-space:nowrap;overflow:hidden}
.sb-h .logo .g{color:var(--pgz-gold)}
.sb-h .sub{font-size:10px;color:var(--t2);margin-top:4px;text-transform:uppercase;letter-spacing:1px;white-space:nowrap;overflow:hidden}
.sb-toggle{position:absolute;top:14px;right:8px;width:22px;height:22px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--t2);background:var(--bg2);border:1px solid var(--rim);border-radius:4px;font-size:14px;font-weight:700;transition:all .15s;user-select:none}
.sb-toggle:hover{background:var(--bg3);color:var(--pgz-gold);border-color:var(--pgz-gold)}
.sb-section-label{padding:10px 14px 4px;font-size:9.5px;color:var(--t4);text-transform:uppercase;letter-spacing:1.2px;font-weight:700;white-space:nowrap;overflow:hidden}
.sb-nav{flex:1;padding:8px 8px;overflow-y:auto;overflow-x:hidden}
.nav-i{padding:9px 12px;border-radius:6px;color:var(--t2);cursor:pointer;display:flex;align-items:center;gap:10px;font-size:12.5px;margin-bottom:2px;transition:background .15s,color .15s;white-space:nowrap;position:relative}
.nav-i:hover{background:var(--bg2);color:var(--t1)}
.nav-i.active{background:linear-gradient(90deg,var(--pgz-blue) 0%,var(--pgz-blue2) 100%);color:#fff;font-weight:600}
.nav-i .ic{width:18px;text-align:center;font-size:14px;flex-shrink:0}
.nav-i .lbl{overflow:hidden;text-overflow:ellipsis}
.nav-i .badge{margin-left:auto;background:var(--red);color:#fff;font-size:9px;font-weight:700;padding:1px 6px;border-radius:8px}
.sb-foot{padding:10px 12px;border-top:1px solid var(--rim);display:flex;align-items:center;gap:8px;white-space:nowrap;overflow:hidden}
.sb-foot .av{width:30px;height:30px;border-radius:50%;background:linear-gradient(135deg,var(--pgz-blue),var(--pgz-gold));color:#fff;font-weight:800;display:flex;align-items:center;justify-content:center;font-size:11px;flex-shrink:0}
.sb-foot .ui{flex:1;min-width:0;overflow:hidden}
.sb-foot .un{font-size:11.5px;color:var(--t1);font-weight:600;line-height:1.2;overflow:hidden;text-overflow:ellipsis}
.sb-foot .ur{font-size:9.5px;color:var(--t4);text-transform:uppercase;letter-spacing:.5px;line-height:1.2;overflow:hidden;text-overflow:ellipsis}
.sb-foot .lo{cursor:pointer;color:var(--t4);font-size:14px;padding:6px 8px;border-radius:5px;transition:all .15s;flex-shrink:0}
.sb-foot .lo:hover{background:rgba(255,45,85,.15);color:var(--red)}
/* Collapsed sidebar */
.sb.collapsed{width:58px}
.sb.collapsed .sb-h{padding:18px 8px 14px;text-align:center}
.sb.collapsed .sb-h .logo{font-size:0}
.sb.collapsed .sb-h .logo::before{content:"PG";font-size:13px;color:var(--pgz-gold);font-weight:800}
.sb.collapsed .sb-h .sub,.sb.collapsed .sb-section-label{display:none}
.sb.collapsed .sb-toggle{position:static;margin:6px auto 0;display:flex}
.sb.collapsed .nav-i{justify-content:center;padding:10px 6px}
.sb.collapsed .nav-i .lbl,.sb.collapsed .nav-i .badge{display:none}
.sb.collapsed .nav-i:hover::after{content:attr(data-label);position:absolute;left:58px;top:50%;transform:translateY(-50%);background:var(--bg3);color:var(--t0);padding:5px 10px;border-radius:4px;font-size:11.5px;white-space:nowrap;border:1px solid var(--rim);z-index:50;font-weight:600;pointer-events:none;box-shadow:2px 2px 8px rgba(0,0,0,.4)}
.sb.collapsed .sb-foot{padding:8px;justify-content:center}
.sb.collapsed .sb-foot .ui{display:none}
.sb.collapsed .sb-foot .lo{display:none}
.main{margin-left:240px;flex:1;min-width:0;transition:margin-left .22s ease}
.sb.collapsed ~ .main{margin-left:58px}
.tb{background:var(--bg1);border-bottom:1px solid var(--rim);padding:12px 22px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:5;gap:12px}
.tb-t{font-size:15px;font-weight:700;color:var(--t0)}
.tb-s{font-size:11px;color:var(--t2)}
.tb-r{display:flex;align-items:center;gap:14px}
.role-switch{display:inline-flex;background:var(--bg2);border:1px solid var(--rim);border-radius:6px;overflow:hidden}
.role-switch button{background:transparent;border:0;padding:6px 12px;color:var(--t2);font-size:11px;font-weight:600;cursor:pointer;letter-spacing:.3px}
.role-switch button:hover{background:var(--bg3);color:var(--t1)}
.role-switch button.active{background:linear-gradient(135deg,var(--pgz-blue),var(--pgz-blue2));color:#fff}
.tb-user{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--t1)}
.tb-user .av{width:28px;height:28px;border-radius:50%;background:linear-gradient(135deg,var(--pgz-blue),var(--pgz-gold));color:#fff;font-weight:800;display:flex;align-items:center;justify-content:center;font-size:11px}
.content{padding:22px}
.section{display:none}
.section.active{display:block}
/* ============ COMPONENTS (shared with sport2.html) ============ */
.kpi-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px;margin-bottom:22px}
.kpi{background:linear-gradient(135deg,var(--bg2) 0%,var(--bg1) 100%);border:1px solid var(--rim);border-radius:8px;padding:14px 16px;position:relative;overflow:hidden;transition:all .18s}
.kpi.click{cursor:pointer}
.kpi.click:hover{border-color:var(--pgz-gold);transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,.4)}
.kpi::before{content:"";position:absolute;top:0;left:0;width:3px;height:100%;background:var(--pgz-gold)}
.kpi.b::before{background:var(--pgz-blue2)}
.kpi.g::before{background:var(--green)}
.kpi.r::before{background:var(--red)}
.kpi.a::before{background:var(--amber)}
.kpi.c::before{background:var(--cyan)}
.kpi-l{font-size:10.5px;color:var(--t2);text-transform:uppercase;letter-spacing:1px;font-weight:600}
.kpi-v{font-size:24px;font-weight:800;color:var(--t0);margin-top:4px;font-family:var(--mono)}
.kpi-s{font-size:10px;color:var(--t4);margin-top:2px}
.kpi-trend{font-size:10px;font-weight:700;margin-top:6px;display:inline-block;padding:1px 6px;border-radius:3px}
.kpi-trend.up{background:rgba(0,232,143,.15);color:var(--green)}
.kpi-trend.down{background:rgba(255,45,85,.15);color:var(--red)}
.card{background:var(--bg2);border:1px solid var(--rim);border-radius:8px;padding:14px;margin-bottom:14px;transition:all .18s}
.card.click-card{cursor:pointer}
.card.click-card:hover{border-color:var(--pgz-gold)}
.card-h{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--rim);gap:10px}
.card-t{font-weight:700;color:var(--t0);font-size:13px}
.card-actions{display:flex;gap:6px}
.row-2{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.row-3{display:grid;grid-template-columns:2fr 1fr;gap:14px}
@media (max-width:900px){.row-2,.row-3{grid-template-columns:1fr}}
.btn{background:var(--bg2);border:1px solid var(--rim);border-radius:5px;padding:7px 12px;color:var(--t1);font-size:12px;cursor:pointer;font-weight:600;transition:all .15s}
.btn:hover{background:var(--bg3);border-color:var(--rim2)}
.btn.primary{background:linear-gradient(135deg,var(--pgz-blue),var(--pgz-blue2));border-color:transparent;color:#fff}
.btn.primary:hover{filter:brightness(1.1)}
.btn.gold{background:var(--pgz-gold);color:var(--bg0);border-color:transparent}
.btn.gold:hover{filter:brightness(1.1)}
.btn.sm{padding:4px 9px;font-size:11px}
table{width:100%;border-collapse:collapse;font-size:12px}
table th{background:var(--bg3);color:var(--t2);text-transform:uppercase;font-size:10px;letter-spacing:.5px;padding:8px 10px;text-align:left;border-bottom:1px solid var(--rim);font-weight:700}
table td{padding:8px 10px;border-bottom:1px solid var(--rim);color:var(--t1)}
table tbody tr{transition:background .15s}
table tbody tr:hover{background:var(--bg3)}
.num{font-family:var(--mono);text-align:right}
.tag{display:inline-block;padding:2px 7px;font-size:10px;border-radius:3px;background:var(--bg4);color:var(--t1);font-weight:600;text-transform:uppercase;letter-spacing:.5px;margin-right:3px}
.tag.b{background:var(--pgz-blue);color:#fff}
.tag.gd{background:var(--pgz-gold);color:var(--bg0)}
.tag.gr{background:var(--green);color:var(--bg0)}
.tag.rd{background:var(--red);color:#fff}
.tag.am{background:var(--amber);color:var(--bg0)}
.tag.cy{background:var(--cyan);color:var(--bg0)}
.audit-i{display:flex;gap:10px;padding:8px 10px;border-bottom:1px solid var(--rim);font-size:11.5px;align-items:flex-start}
.audit-i:last-child{border:0}
.audit-i .ts{color:var(--t4);font-family:var(--mono);font-size:10.5px;flex-shrink:0;width:90px}
.audit-i .who{color:var(--pgz-gold);font-weight:600;flex-shrink:0;width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.audit-i .what{color:var(--t1);flex:1;min-width:0}
.audit-i .what b{color:var(--cyan)}
.req-i{padding:10px 12px;border:1px solid var(--rim);border-radius:6px;margin-bottom:8px;background:var(--bg2);transition:all .15s;cursor:pointer}
.req-i:hover{border-color:var(--pgz-gold)}
.req-i .rh{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}
.req-i .rt{font-weight:700;color:var(--t0);font-size:12.5px}
.req-i .rsum{font-size:11px;color:var(--t2);margin-top:2px;line-height:1.4}
.req-i .rmeta{display:flex;gap:10px;margin-top:6px;font-size:10.5px;color:var(--t4)}
.req-i .rmeta b{color:var(--pgz-gold);font-weight:700}
.member-i{display:flex;align-items:center;gap:10px;padding:8px 10px;border-bottom:1px solid var(--rim)}
.member-i:last-child{border:0}
.member-i .av{width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,var(--bg3),var(--bg4));color:var(--t0);font-weight:800;display:flex;align-items:center;justify-content:center;font-size:11px;flex-shrink:0}
.member-i .mn{font-weight:700;color:var(--t0);font-size:12px}
.member-i .mp{font-size:10.5px;color:var(--t2)}
.member-i .mright{margin-left:auto;text-align:right}
.cal-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:4px}
.cal-h{font-size:10px;color:var(--t4);text-transform:uppercase;text-align:center;padding:4px 0;font-weight:700;letter-spacing:.5px}
.cal-d{aspect-ratio:1;background:var(--bg3);border:1px solid var(--rim);border-radius:4px;padding:4px;font-size:10.5px;color:var(--t2);position:relative;cursor:pointer}
.cal-d:hover{border-color:var(--pgz-gold)}
.cal-d.t{border-color:var(--pgz-gold);background:var(--bg4)}
.cal-d.has-event::after{content:"";position:absolute;bottom:3px;left:50%;transform:translateX(-50%);width:5px;height:5px;background:var(--pgz-gold);border-radius:50%}
.profile-card{display:grid;grid-template-columns:120px 1fr;gap:18px;padding:14px}
.profile-photo{width:120px;height:140px;border-radius:8px;background:linear-gradient(135deg,var(--bg3),var(--bg4));display:flex;align-items:center;justify-content:center;font-size:48px;color:var(--t4);font-weight:800;overflow:hidden;cursor:pointer;border:2px solid var(--rim);transition:all .2s}
.profile-photo:hover{border-color:var(--pgz-gold)}
.profile-info h2{font-size:20px;color:var(--t0);margin-bottom:4px}
.profile-info .sub{font-size:12px;color:var(--t2);margin-bottom:8px}
.profile-info .tags-row{margin-bottom:10px}
.kv{display:grid;grid-template-columns:160px 1fr;gap:6px 12px;font-size:12px}
.kv .k{color:var(--t2);font-weight:600}
.kv .v{color:var(--t1);word-break:break-word}
.alert-card{padding:10px 12px;border-left:3px solid var(--amber);background:var(--bg2);border-radius:5px;margin-bottom:8px}
.alert-card.crit{border-color:var(--red)}
.alert-card.ok{border-color:var(--green)}
.alert-card .at{font-weight:700;font-size:12px;color:var(--t0)}
.alert-card .ad{font-size:11px;color:var(--t2);margin-top:3px}
.empty{text-align:center;padding:30px;color:var(--t4);font-size:12px;font-style:italic}
.loading{padding:24px;text-align:center;color:var(--t2);font-size:12px}
.loading::before{content:"";display:inline-block;width:12px;height:12px;border:2px solid var(--rim);border-top-color:var(--pgz-gold);border-radius:50%;animation:spin .8s linear infinite;margin-right:8px;vertical-align:middle}
@keyframes spin{to{transform:rotate(360deg)}}
.chart-box{background:var(--bg2);border:1px solid var(--rim);border-radius:8px;padding:14px;height:280px}
.chart-box canvas{max-height:240px}
.demo-banner{background:linear-gradient(90deg,rgba(244,196,48,.15),rgba(0,76,196,.1));border:1px solid var(--pgz-gold);border-radius:6px;padding:10px 14px;margin-bottom:14px;font-size:11.5px;color:var(--t1);display:flex;align-items:center;gap:10px}
.demo-banner b{color:var(--pgz-gold)}
@media (max-width:768px){
.sb{transform:translateX(-100%);transition:transform .25s}
.sb.open{transform:translateX(0)}
.main,.sb.collapsed ~ .main{margin-left:0}
.role-switch{display:none}
}
</style>
</head>
<body>
<div class="app">
<aside class="sb" id="sb">
<div class="sb-h">
<div class="logo">PGŽ <span class="g">SPORT</span></div>
<div class="sub" id="role-sub">Operativna aplikacija</div>
<div class="sb-toggle" id="sb-toggle" onclick="toggleSidebar()" title="Skupi/raširi sidebar">≡</div>
</div>
<div class="sb-section-label" id="role-section-label">Navigacija</div>
<nav class="sb-nav" id="nav"></nav>
<div class="sb-foot" id="sb-foot">
<div class="av" id="sf-av">DR</div>
<div class="ui">
<div class="un" id="sf-name">Damir Radulić</div>
<div class="ur" id="sf-role">PGŽ admin</div>
</div>
<div class="lo" onclick="logout()" title="Odjava">⎋</div>
</div>
</aside>
<main class="main">
<div class="tb">
<div>
<div class="tb-t" id="tb-t">Dashboard</div>
<div class="tb-s" id="tb-s">Pregled stanja</div>
</div>
<div class="tb-r">
<div class="role-switch" id="role-switch"></div>
<div class="tb-user">
<div class="av" id="user-av">DR</div>
<div>
<div style="font-weight:700" id="user-name">Damir Radulić</div>
<div style="font-size:10px;color:var(--t4)" id="user-role-label">PGŽ admin</div>
</div>
</div>
</div>
</div>
<div class="content" id="content">
<div class="loading">Učitavanje...</div>
</div>
</main>
</div>
<script>
//=========== UTIL ===========
const API = '/sport/api';
const $ = s => document.querySelector(s);
const $$ = s => document.querySelectorAll(s);
const esc = s => String(s==null?'':s).replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]));
const fmt = n => (n==null?'—':Number(n).toLocaleString('hr-HR'));
const fmtEur = n => (n==null?'—':Number(n).toLocaleString('hr-HR',{maximumFractionDigits:0})+' €');
async function api(path){
try { const r = await fetch(API+path); if(!r.ok) return null; return await r.json(); }
catch(e){ return null; }
}
//=========== ROLES ===========
const ROLES = {
pgz: {name:'PGŽ admin', user:'Damir Radulić', av:'DR', sub:'Odjel za sport · PGŽ'},
savez: {name:'Savez admin', user:'Marija Kovač', av:'MK', sub:'Atletski savez PGŽ'},
klub: {name:'Klub admin', user:'Igor Tomić', av:'IT', sub:'AK Kvarner Rijeka'},
sportas:{name:'Sportaš', user:'Luka Horvat', av:'LH', sub:'AK Kvarner Rijeka · Trčanje 800m'},
};
const NAV_BY_ROLE = {
pgz: [
{id:'dashboard', ic:'\u{1F4CA}', label:'Dashboard'},
{id:'korisnici', ic:'\u{1F465}', label:'Korisnici'},
{id:'savezi', ic:'\u{1F3C5}', label:'Savezi'},
{id:'klubovi', ic:'⬢', label:'Klubovi'},
{id:'sportasi', ic:'\u{1F464}', label:'Sportaši'},
{id:'financije', ic:'€', label:'Financije'},
{id:'racuni', ic:'\u{1F9FE}', label:'Računi (OCR)'},
{id:'crm', ic:'\u{1F4DD}', label:'CRM'},
{id:'audit', ic:'\u{1F50D}', label:'Audit log'},
{id:'forenzika', ic:'⚠', label:'Forenzika', badge:11},
],
savez: [
{id:'dashboard', ic:'\u{1F4CA}', label:'Dashboard'},
{id:'klubovi', ic:'⬢', label:'Naši klubovi'},
{id:'sportasi', ic:'\u{1F464}', label:'Naši sportaši'},
{id:'zahtjevi', ic:'\u{1F4D1}', label:'Zahtjevi PGŽ', badge:3},
{id:'kalendar', ic:'\u{1F4C5}', label:'Kalendar'},
{id:'lijecnicki',ic:'⚕', label:'Liječnički'},
{id:'racuni', ic:'\u{1F9FE}', label:'Računi (OCR)'},
],
klub: [
{id:'dashboard', ic:'\u{1F4CA}', label:'Dashboard'},
{id:'clanovi', ic:'\u{1F465}', label:'Članovi'},
{id:'clanarine', ic:'€', label:'Članarine'},
{id:'lijecnicki',ic:'⚕', label:'Liječnički'},
{id:'dokumenti', ic:'\u{1F4C4}', label:'Dokumenti'},
{id:'manifestacije', ic:'\u{1F4C5}', label:'Manifestacije'},
{id:'racuni', ic:'\u{1F9FE}', label:'Računi (OCR)'},
],
sportas: [
{id:'dashboard', ic:'\u{1F4CA}', label:'Moj profil'},
{id:'clanarina', ic:'€', label:'Članarina'},
{id:'lijecnicki',ic:'⚕', label:'Liječnički'},
{id:'dokumenti', ic:'\u{1F4C4}', label:'Moji dokumenti'},
{id:'obrasci', ic:'\u{1F4DD}', label:'Obrasci', badge:1},
{id:'manifestacije', ic:'\u{1F4C5}', label:'Manifestacije'},
],
};
const _state = {role:'pgz', section:'dashboard'};
//=========== SIDEBAR ===========
function toggleSidebar(){
const sb = $('#sb');
const tg = $('#sb-toggle');
if(!sb) return;
const c = sb.classList.toggle('collapsed');
if(tg) tg.textContent = '≡';
try { localStorage.setItem('sidebar-state', c ? 'collapsed' : 'expanded'); } catch(e){}
}
function restoreSidebar(){
try {
if(localStorage.getItem('sidebar-state') === 'collapsed') $('#sb').classList.add('collapsed');
} catch(e){}
}
//=========== ROLE SWITCH ===========
function buildRoleSwitch(){
const rs = $('#role-switch');
rs.innerHTML = Object.entries(ROLES).map(([k,r]) =>
`<button data-role="${k}" onclick="setRole('${k}')" class="${k===_state.role?'active':''}">${esc(r.name)}</button>`
).join('');
}
function setRole(r){
if(!ROLES[r]) return;
_state.role = r;
_state.section = 'dashboard';
try { localStorage.setItem('app-role', r); } catch(e){}
$$('.role-switch button').forEach(b => b.classList.toggle('active', b.dataset.role===r));
const role = ROLES[r];
$('#user-name').textContent = role.user;
$('#user-av').textContent = role.av;
$('#user-role-label').textContent = role.name;
$('#role-sub').textContent = role.sub;
$('#role-section-label').textContent = role.name.toUpperCase();
$('#sf-name').textContent = role.user;
$('#sf-role').textContent = role.name;
$('#sf-av').textContent = role.av;
buildNav();
navTo('dashboard');
}
//=========== NAV ===========
function buildNav(){
const items = NAV_BY_ROLE[_state.role] || [];
$('#nav').innerHTML = items.map(n =>
`<div class="nav-i ${n.id===_state.section?'active':''}" data-id="${n.id}" data-label="${esc(n.label)}" onclick="navTo('${n.id}')">
<span class="ic">${n.ic}</span>
<span class="lbl">${esc(n.label)}</span>
${n.badge?`<span class="badge">${n.badge}</span>`:''}
</div>`
).join('');
}
function navTo(id){
_state.section = id;
$$('.nav-i').forEach(el => el.classList.toggle('active', el.dataset.id===id));
loadSection();
}
function logout(){
if(!confirm('Odjava iz aplikacije?')) return;
try {
localStorage.removeItem('app-role');
localStorage.removeItem('jwt');
} catch(e){}
alert('Odjavljen. (Production: redirect na /login)');
window.location.href = '/sport/static/sport2.html';
}
//=========== SECTION TITLES ===========
const TITLES = {
pgz: {
dashboard:['Dashboard','Pregled stanja PGŽ Sporta'],
korisnici:['Korisnici','Upravljanje korisnicima sustava'],
savezi:['Savezi','246 sportskih saveza'],
klubovi:['Klubovi','Sportski klubovi PGŽ'],
sportasi:['Sportaši','Registrirani članovi'],
financije:['Financije','Sufinanciranje sporta'],
racuni:['Računi (OCR)','OCR upload + obrada'],
crm:['CRM','Članarine + liječnički'],
audit:['Audit log','Sve aktivnosti sustava'],
forenzika:['Forenzika','Sumnjive transakcije / PEP'],
},
savez: {
dashboard:['Dashboard','Atletski savez PGŽ'],
klubovi:['Naši klubovi','Klubovi člana saveza'],
sportasi:['Naši sportaši','Registrirani sportaši saveza'],
zahtjevi:['Zahtjevi PGŽ','Sufinanciranje aktivnosti'],
kalendar:['Kalendar','Manifestacije saveza'],
lijecnicki:['Liječnički','Pregledi članova saveza'],
racuni:['Računi','Računi saveza'],
},
klub: {
dashboard:['Dashboard','AK Kvarner Rijeka'],
clanovi:['Članovi','Članovi kluba'],
clanarine:['Članarine','Stanje članarina'],
lijecnicki:['Liječnički','Pregledi članova'],
dokumenti:['Dokumenti','Dokumenti kluba'],
manifestacije:['Manifestacije','Nadolazeće aktivnosti'],
racuni:['Računi','Troškovi kluba'],
},
sportas: {
dashboard:['Moj profil','Luka Horvat'],
clanarina:['Članarina','Stanje moje članarine'],
lijecnicki:['Liječnički','Moj liječnički pregled'],
dokumenti:['Moji dokumenti','Suglasnosti, ugovori'],
obrasci:['Obrasci','Za potpis'],
manifestacije:['Manifestacije','Moje aktivnosti'],
},
};
function loadSection(){
const id = _state.section;
const role = _state.role;
const t = (TITLES[role] && TITLES[role][id]) || [id,''];
$('#tb-t').textContent = t[0];
$('#tb-s').textContent = t[1];
const fn = SECTIONS[role+':'+id] || SECTIONS[role+':default'] || (() => '<div class="empty">Sekcija u izradi.</div>');
$('#content').innerHTML = '<div class="loading">Učitavanje...</div>';
Promise.resolve(fn()).then(html => { $('#content').innerHTML = html || '<div class="empty">Nema podataka.</div>'; });
}
//=========== SECTION RENDERERS ===========
const SECTIONS = {};
// =======================================================================
// PGŽ ADMIN — Dashboard
// =======================================================================
SECTIONS['pgz:dashboard'] = async () => {
const d = await api('/dashboard') || {};
const kpis = `
<div class="kpi-grid">
<div class="kpi b click" onclick="navTo('savezi')"><div class="kpi-l">Saveza</div><div class="kpi-v">${fmt(d.aktivnih_saveza||246)}</div><div class="kpi-s">aktivnih</div><span class="kpi-trend up">+2 ovaj mj.</span></div>
<div class="kpi click" onclick="navTo('klubovi')"><div class="kpi-l">Klubova</div><div class="kpi-v">${fmt(d.aktivnih_klubova||1656)}</div><div class="kpi-s">registriranih</div></div>
<div class="kpi g click" onclick="navTo('sportasi')"><div class="kpi-l">Sportaša</div><div class="kpi-v">${fmt(d.aktivnih_clanova||3243)}</div><div class="kpi-s">aktivnih</div><span class="kpi-trend up">+45 ovaj mj.</span></div>
<div class="kpi a click" onclick="navTo('financije')"><div class="kpi-l">Proračun 2026</div><div class="kpi-v">${fmtEur(d.proracun_aktualni||2817309)}</div><div class="kpi-s">odobreno</div></div>
<div class="kpi r click" onclick="navTo('forenzika')"><div class="kpi-l">Critical alerts</div><div class="kpi-v">${fmt(d.critical_alerts||11)}</div><div class="kpi-s">forenzika</div></div>
<div class="kpi c click" onclick="navTo('crm')"><div class="kpi-l">Ist. liječničkih</div><div class="kpi-v">${fmt(d.isteki_lijecnicki||11)}</div><div class="kpi-s">treba obnoviti</div></div>
</div>`;
const reqHtml = MOCK.zahtjevi_pending.map(z => `
<div class="req-i" onclick="alert('Otvaranje zahtjeva ${esc(z.id)} — production: navigira na detalj')">
<div class="rh">
<div>
<div class="rt">${esc(z.naziv)}</div>
<div class="rsum">${esc(z.savez)} · ${esc(z.svrha)}</div>
</div>
<div><span class="tag am">${esc(z.status)}</span></div>
</div>
<div class="rmeta">
<div>Iznos: <b>${fmtEur(z.iznos)}</b></div>
<div>Predano: ${esc(z.datum)}</div>
<div>Klub: ${esc(z.klub||'—')}</div>
</div>
</div>`).join('');
const auditHtml = MOCK.audit.map(a =>
`<div class="audit-i"><div class="ts">${esc(a.ts)}</div><div class="who">${esc(a.who)}</div><div class="what">${a.what}</div></div>`
).join('');
return `
<div class="demo-banner">
<span style="font-size:18px">🛡️</span>
<div><b>PGŽ admin view</b> — najviša razina ovlaštenja. Vidiš sve saveze, klubove i transakcije.</div>
</div>
${kpis}
<div class="row-3">
<div>
<div class="card">
<div class="card-h">
<div class="card-t">📋 Zahtjevi za sufinanciranje (${MOCK.zahtjevi_pending.length} čeka)</div>
<div class="card-actions"><button class="btn primary sm" onclick="navTo('financije')">Svi →</button></div>
</div>
${reqHtml || '<div class="empty">Nema zahtjeva.</div>'}
</div>
<div class="card">
<div class="card-h"><div class="card-t">📊 Trend isplata 20252026</div></div>
<div class="chart-box"><canvas id="ch-trend"></canvas></div>
</div>
</div>
<div>
<div class="card">
<div class="card-h"><div class="card-t">⚡ Brze akcije</div></div>
<div style="display:grid;gap:8px">
<button class="btn primary" onclick="setRole('pgz');navTo('korisnici')">+ Dodaj korisnika</button>
<button class="btn" onclick="navTo('forenzika')">⚠ Pregled forenzike</button>
<button class="btn" onclick="navTo('racuni')">🧾 Skeniraj račun (OCR)</button>
<button class="btn" onclick="navTo('audit')">🔍 Audit log</button>
<button class="btn gold" onclick="window.open('/sport/','_blank')">🌐 Public portal</button>
</div>
</div>
<div class="card">
<div class="card-h"><div class="card-t">🔍 Audit log (zadnjih 6)</div></div>
${auditHtml}
</div>
</div>
</div>`;
};
// chart hook executed after innerHTML
const _origLoad = loadSection;
loadSection = function(){
_origLoad();
setTimeout(() => {
const c = document.getElementById('ch-trend');
if(c && window.Chart){
try {
new Chart(c, {
type:'line',
data:{
labels:['I','II','III','IV','V','VI','VII','VIII','IX','X','XI','XII'],
datasets:[
{label:'2025',data:[180,220,240,280,310,350,290,320,360,400,420,440],borderColor:'#8a95b4',backgroundColor:'rgba(138,149,180,.15)',tension:.35},
{label:'2026',data:[210,260,290,330,380,420,null,null,null,null,null,null],borderColor:'#F4C430',backgroundColor:'rgba(244,196,48,.18)',tension:.35,fill:true},
],
},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{labels:{color:'#e2e6f0',font:{size:11}}}},scales:{x:{ticks:{color:'#8a95b4'},grid:{color:'#1e2a50'}},y:{ticks:{color:'#8a95b4'},grid:{color:'#1e2a50'}}}}
});
} catch(e){}
}
}, 80);
};
// PGŽ admin sub-pages
SECTIONS['pgz:korisnici'] = () => {
const rows = MOCK.korisnici.map(u => `
<tr>
<td><b>${esc(u.ime)}</b></td>
<td>${esc(u.email)}</td>
<td><span class="tag b">${esc(u.role)}</span></td>
<td>${esc(u.tenant)}</td>
<td>${esc(u.status)==='aktivan'?'<span class="tag gr">aktivan</span>':'<span class="tag rd">suspended</span>'}</td>
<td>${esc(u.last_login)}</td>
<td><button class="btn sm" onclick="alert('Audit za korisnika ${esc(u.email)}')">Audit</button></td>
</tr>`).join('');
return `
<div class="card">
<div class="card-h">
<div class="card-t">👥 Korisnici sustava (${MOCK.korisnici.length})</div>
<div class="card-actions">
<button class="btn" onclick="alert('Bulk import iz CSV')">📥 Import CSV</button>
<button class="btn primary" onclick="alert('Wizard za dodavanje korisnika')">+ Novi korisnik</button>
</div>
</div>
<table>
<thead><tr><th>Ime</th><th>Email</th><th>Uloga</th><th>Tenant</th><th>Status</th><th>Zadnji login</th><th></th></tr></thead>
<tbody>${rows}</tbody>
</table>
</div>`;
};
SECTIONS['pgz:savezi'] = async () => {
const d = await api('/savezi') || {rows:[]};
const top = (d.rows||[]).slice(0,30);
const rows = top.map(s => `
<tr>
<td><b>${esc(s.naziv)}</b></td>
<td class="num">${fmt(s.broj_klubova||'—')}</td>
<td class="num">${fmt(s.broj_sportasa||'—')}</td>
<td>${esc(s.predsjednik||'—')}</td>
<td><button class="btn sm" onclick="window.open('/sport/?savez=${s.id}','_blank')">Detalji</button></td>
</tr>`).join('');
return `<div class="card"><div class="card-h"><div class="card-t">🏅 Savezi PGŽ — top 30 (od ${d.count||246})</div></div>
<table><thead><tr><th>Naziv</th><th class="num">Klubovi</th><th class="num">Sportaši</th><th>Predsjednik</th><th></th></tr></thead><tbody>${rows||'<tr><td colspan=5 class="empty">Učitavam...</td></tr>'}</tbody></table>
</div>`;
};
SECTIONS['pgz:klubovi'] = async () => {
const d = await api('/klubovi?limit=40') || {rows:[]};
const rows = (d.rows||[]).slice(0,40).map(k => `
<tr>
<td><b>${esc(k.naziv)}</b></td>
<td>${esc(k.savez||'—')}</td>
<td>${esc(k.grad||'—')}</td>
<td class="num">${fmt(k.broj_clanova||'—')}</td>
<td>${esc(k.predsjednik||'—')}</td>
</tr>`).join('');
return `<div class="card"><div class="card-h"><div class="card-t">⬢ Klubovi (${d.count||0})</div></div>
<table><thead><tr><th>Naziv</th><th>Savez</th><th>Grad</th><th class="num">Članova</th><th>Predsjednik</th></tr></thead><tbody>${rows||'<tr><td colspan=5 class="empty">—</td></tr>'}</tbody></table>
</div>`;
};
SECTIONS['pgz:sportasi'] = async () => {
const d = await api('/clanovi?limit=40') || {rows:[]};
const rows = (d.rows||[]).slice(0,40).map(c => `
<tr>
<td><b>${esc(c.ime+' '+(c.prezime||''))}</b></td>
<td>${esc(c.klub||'—')}</td>
<td>${esc(c.kategorija||'—')}</td>
<td>${esc(c.spol||'—')}</td>
<td>${esc(c.datum_rodjenja||'—')}</td>
</tr>`).join('');
return `<div class="card"><div class="card-h"><div class="card-t">👤 Sportaši (${d.count||0})</div></div>
<table><thead><tr><th>Ime i prezime</th><th>Klub</th><th>Kategorija</th><th>Spol</th><th>Rođen</th></tr></thead><tbody>${rows||'<tr><td colspan=5 class="empty">—</td></tr>'}</tbody></table>
</div>`;
};
SECTIONS['pgz:financije'] = async () => {
const d = await api('/proracun') || {};
return `
<div class="kpi-grid">
<div class="kpi"><div class="kpi-l">Proračun 2026</div><div class="kpi-v">${fmtEur(d.proracun||2817309)}</div></div>
<div class="kpi g"><div class="kpi-l">Isplaćeno</div><div class="kpi-v">${fmtEur(d.isplaceno||1240000)}</div></div>
<div class="kpi a"><div class="kpi-l">U obradi</div><div class="kpi-v">${fmtEur(d.u_obradi||320000)}</div></div>
<div class="kpi r"><div class="kpi-l">Odbijeno</div><div class="kpi-v">${fmtEur(d.odbijeno||45000)}</div></div>
</div>
<div class="card"><div class="card-h"><div class="card-t">📋 Pending zahtjevi za sufinanciranje</div></div>
${MOCK.zahtjevi_pending.map(z => `
<div class="req-i">
<div class="rh"><div><div class="rt">${esc(z.naziv)}</div><div class="rsum">${esc(z.savez)} · ${esc(z.svrha)}</div></div>
<div><span class="tag am">${esc(z.status)}</span> <button class="btn sm primary">Odobri</button> <button class="btn sm">Odbij</button></div></div>
<div class="rmeta"><div>Iznos: <b>${fmtEur(z.iznos)}</b></div><div>Predano: ${esc(z.datum)}</div></div>
</div>`).join('')}
</div>`;
};
SECTIONS['pgz:racuni'] = () => `
<div class="card">
<div class="card-h"><div class="card-t">🧾 OCR upload (drag & drop)</div></div>
<div style="border:2px dashed var(--rim2);border-radius:8px;padding:40px;text-align:center;background:var(--bg3);cursor:pointer" onclick="alert('OCR upload — backend M5 (CC4)')">
<div style="font-size:48px;margin-bottom:8px">📷</div>
<div style="font-weight:700;color:var(--t0);margin-bottom:4px">Dovuci ovdje sliku ili PDF računa</div>
<div style="font-size:11px;color:var(--t2)">ili klikni za odabir · cestarina, gorivo, hotel, dnevnice...</div>
<button class="btn primary" style="margin-top:12px">📸 Snimi kamerom</button>
</div>
<div style="font-size:11px;color:var(--t4);margin-top:10px">Backend: Tesseract OCR + DeepSeek V3 ekstrakcija polja → invoices DB</div>
</div>
<div class="card">
<div class="card-h"><div class="card-t">📋 Nedavni računi</div></div>
<table><thead><tr><th>Datum</th><th>Izdavatelj</th><th>OIB</th><th>Vrsta</th><th class="num">Iznos</th><th>Status</th></tr></thead>
<tbody>${MOCK.invoices.map(r => `<tr><td>${esc(r.datum)}</td><td><b>${esc(r.izdavatelj)}</b></td><td>${esc(r.oib)}</td><td><span class="tag ${r.tag}">${esc(r.vrsta)}</span></td><td class="num">${fmtEur(r.iznos)}</td><td>${esc(r.status)}</td></tr>`).join('')}</tbody></table>
</div>`;
SECTIONS['pgz:crm'] = () => `
<div style="margin-bottom:12px">
<a href="/sport/crm" target="_blank" class="btn primary" style="text-decoration:none">📋 Otvori CRM workspace (Članarine • Liječnički • Obrasci) — live API</a>
</div>
<div class="row-2">
<div class="card">
<div class="card-h"><div class="card-t">€ Članarine 2026</div></div>
<div class="kpi-grid" style="margin-bottom:0">
<div class="kpi g"><div class="kpi-l">Naplaćeno</div><div class="kpi-v">${fmtEur(5400)}</div></div>
<div class="kpi r"><div class="kpi-l">Dug</div><div class="kpi-v">${fmtEur(720)}</div></div>
</div>
<div style="margin-top:12px">
<button class="btn primary">📧 Notifikacija svima koji duguju</button>
<button class="btn">📄 Generiraj HUB-3 batch</button>
</div>
</div>
<div class="card">
<div class="card-h"><div class="card-t">⚕ Liječnički pregledi</div></div>
<div class="kpi-grid" style="margin-bottom:0">
<div class="kpi g"><div class="kpi-l">Validni</div><div class="kpi-v">${fmt(3232)}</div></div>
<div class="kpi a"><div class="kpi-l">Uskoro istek (30d)</div><div class="kpi-v">0</div></div>
<div class="kpi r"><div class="kpi-l">Istekli</div><div class="kpi-v">11</div></div>
</div>
<div style="margin-top:12px"><button class="btn primary">📅 ZZJZ PGŽ rezervacija</button></div>
</div>
</div>`;
SECTIONS['pgz:audit'] = () => `
<div class="card">
<div class="card-h"><div class="card-t">🔍 Audit log — sve aktivnosti</div>
<div class="card-actions">
<input type="text" placeholder="Pretraži korisnika..." style="background:var(--bg2);border:1px solid var(--rim);border-radius:5px;padding:6px 10px;color:var(--t1);font-size:12px">
</div>
</div>
${MOCK.audit.concat(MOCK.audit_more).map(a =>
`<div class="audit-i"><div class="ts">${esc(a.ts)}</div><div class="who">${esc(a.who)}</div><div class="what">${a.what}</div></div>`
).join('')}
</div>`;
SECTIONS['pgz:forenzika'] = () => `
<div class="card">
<div class="card-h"><div class="card-t">⚠ Forenzika — sumnjive transakcije</div></div>
${MOCK.forenzika.map(f => `
<div class="alert-card ${f.sev==='crit'?'crit':''}">
<div class="at">${esc(f.title)}</div>
<div class="ad">${esc(f.desc)}</div>
<div style="margin-top:6px"><span class="tag ${f.sev==='crit'?'rd':'am'}">${esc(f.sev)}</span> <span class="tag">${esc(f.tip)}</span></div>
</div>`).join('')}
</div>`;
// =======================================================================
// SAVEZ ADMIN — Dashboard + sub-pages
// =======================================================================
SECTIONS['savez:dashboard'] = () => {
const klubHtml = MOCK.savez_klubovi.map(k => `
<div class="member-i">
<div class="av">${esc(k.naziv.substring(0,2).toUpperCase())}</div>
<div>
<div class="mn">${esc(k.naziv)}</div>
<div class="mp">${esc(k.grad)} · ${fmt(k.clanova)} članova</div>
</div>
<div class="mright">
${k.alert?'<span class="tag am">⚠</span>':'<span class="tag gr">OK</span>'}
</div>
</div>`).join('');
return `
<div class="demo-banner">
<span style="font-size:18px">🏅</span>
<div><b>Savez admin view</b> — vidiš sve klubove i sportaše u svom savezu (Atletski savez PGŽ).</div>
</div>
<div class="kpi-grid">
<div class="kpi b click" onclick="navTo('klubovi')"><div class="kpi-l">Naših klubova</div><div class="kpi-v">12</div><div class="kpi-s">aktivnih</div></div>
<div class="kpi g click" onclick="navTo('sportasi')"><div class="kpi-l">Sportaša</div><div class="kpi-v">487</div><div class="kpi-s">registriranih</div></div>
<div class="kpi a click" onclick="navTo('zahtjevi')"><div class="kpi-l">Zahtjevi PGŽ</div><div class="kpi-v">3</div><div class="kpi-s">u obradi</div></div>
<div class="kpi r click" onclick="navTo('lijecnicki')"><div class="kpi-l">Liječnički ist.</div><div class="kpi-v">7</div><div class="kpi-s">do 30 dana</div></div>
</div>
<div class="row-2">
<div class="card">
<div class="card-h"><div class="card-t">⬢ Naši klubovi (12)</div><div class="card-actions"><button class="btn primary sm" onclick="navTo('klubovi')">Svi →</button></div></div>
${klubHtml}
</div>
<div class="card">
<div class="card-h"><div class="card-t">📑 Naši zahtjevi PGŽ-u</div></div>
${MOCK.savez_zahtjevi.map(z => `
<div class="req-i">
<div class="rh"><div class="rt">${esc(z.naziv)}</div>
<div><span class="tag ${z.tag}">${esc(z.status)}</span></div>
</div>
<div class="rmeta"><div>Iznos: <b>${fmtEur(z.iznos)}</b></div><div>Predano: ${esc(z.datum)}</div></div>
</div>`).join('')}
</div>
</div>
<div class="card">
<div class="card-h"><div class="card-t">⚕ Liječnički pregledi koji ističu (uskoro)</div></div>
<table>
<thead><tr><th>Sportaš</th><th>Klub</th><th>Datum isteka</th><th>Dana do isteka</th><th></th></tr></thead>
<tbody>${MOCK.lijecnicki_uskoro.map(l => `<tr><td><b>${esc(l.ime)}</b></td><td>${esc(l.klub)}</td><td>${esc(l.datum)}</td><td><span class="tag am">${l.dana} dana</span></td><td><button class="btn sm">Zakazat ZZJZ</button></td></tr>`).join('')}</tbody>
</table>
</div>`;
};
SECTIONS['savez:klubovi'] = () => `
<div class="card"><div class="card-h"><div class="card-t">⬢ Klubovi Atletskog saveza PGŽ (12)</div></div>
<table><thead><tr><th>Klub</th><th>Grad</th><th class="num">Članova</th><th>Predsjednik</th><th>Status</th></tr></thead>
<tbody>${MOCK.savez_klubovi_full.map(k => `<tr><td><b>${esc(k.naziv)}</b></td><td>${esc(k.grad)}</td><td class="num">${fmt(k.clanova)}</td><td>${esc(k.predsjednik)}</td><td>${k.alert?'<span class="tag am">⚠ Liječnički</span>':'<span class="tag gr">OK</span>'}</td></tr>`).join('')}</tbody></table>
</div>`;
SECTIONS['savez:sportasi'] = () => `
<div class="card"><div class="card-h"><div class="card-t">👤 Naši sportaši (487)</div></div>
<div class="kpi-grid">
<div class="kpi"><div class="kpi-l">Senior</div><div class="kpi-v">142</div></div>
<div class="kpi b"><div class="kpi-l">Juniori</div><div class="kpi-v">98</div></div>
<div class="kpi g"><div class="kpi-l">Mladi</div><div class="kpi-v">156</div></div>
<div class="kpi a"><div class="kpi-l">Reprezent.</div><div class="kpi-v">22</div></div>
</div>
<table><thead><tr><th>Ime i prezime</th><th>Klub</th><th>Disciplina</th><th>Kategorija</th><th>Liječnički</th></tr></thead>
<tbody>${MOCK.savez_sportasi.map(s => `<tr><td><b>${esc(s.ime)}</b></td><td>${esc(s.klub)}</td><td>${esc(s.disciplina)}</td><td><span class="tag b">${esc(s.kat)}</span></td><td>${s.lijecnicki==='ok'?'<span class="tag gr">OK</span>':'<span class="tag am">istek</span>'}</td></tr>`).join('')}</tbody></table>
</div>`;
SECTIONS['savez:zahtjevi'] = () => `
<div class="card"><div class="card-h"><div class="card-t">📑 Naši zahtjevi za sufinanciranje</div><div class="card-actions"><button class="btn primary">+ Novi zahtjev</button></div></div>
${MOCK.savez_zahtjevi.concat(MOCK.savez_zahtjevi_more).map(z => `
<div class="req-i">
<div class="rh"><div><div class="rt">${esc(z.naziv)}</div><div class="rsum">${esc(z.svrha||'')}</div></div>
<div><span class="tag ${z.tag}">${esc(z.status)}</span></div></div>
<div class="rmeta"><div>Iznos: <b>${fmtEur(z.iznos)}</b></div><div>Predano: ${esc(z.datum)}</div></div>
</div>`).join('')}
</div>`;
SECTIONS['savez:kalendar'] = () => `
<div class="card"><div class="card-h"><div class="card-t">📅 Kalendar manifestacija — Svibanj 2026</div></div>
<div class="cal-grid">
${'PON UTO SRI ČET PET SUB NED'.split(' ').map(h => `<div class="cal-h">${h}</div>`).join('')}
${[...Array(31)].map((_,i) => {
const day = i+1;
const ev = [4,11,18,25,9,16,30].includes(day);
const today = day===5;
return `<div class="cal-d ${today?'t':''} ${ev?'has-event':''}"><b>${day}</b>${ev?`<div style="font-size:9px;color:var(--pgz-gold);margin-top:2px">Trening / utakmica</div>`:''}</div>`;
}).join('')}
</div>
<div style="margin-top:14px;font-size:11px;color:var(--t2)">● Trening kamp Platak (46.5) · ● Liga PGŽ atletika (11.5) · ● Open senior (18.5) · ● Memorijalna utrka (25.5)</div>
</div>`;
SECTIONS['savez:lijecnicki'] = () => `
<div class="card"><div class="card-h"><div class="card-t">⚕ Liječnički pregledi članova saveza</div><div class="card-actions"><button class="btn primary">📅 Bulk ZZJZ rezervacija</button></div></div>
<div class="kpi-grid">
<div class="kpi g"><div class="kpi-l">Validni</div><div class="kpi-v">468</div></div>
<div class="kpi a"><div class="kpi-l">Uskoro istek</div><div class="kpi-v">7</div></div>
<div class="kpi r"><div class="kpi-l">Istekli</div><div class="kpi-v">12</div></div>
</div>
<table><thead><tr><th>Sportaš</th><th>Klub</th><th>Vrijedi do</th><th>Doktor</th><th>Status</th><th></th></tr></thead>
<tbody>${MOCK.lijecnicki_uskoro.map(l => `<tr><td><b>${esc(l.ime)}</b></td><td>${esc(l.klub)}</td><td>${esc(l.datum)}</td><td>${esc(l.doktor||'Dr. Marković')}</td><td><span class="tag am">${l.dana} dana</span></td><td><button class="btn sm">Zakaži</button></td></tr>`).join('')}</tbody></table>
</div>`;
SECTIONS['savez:racuni'] = SECTIONS['pgz:racuni'];
// =======================================================================
// KLUB ADMIN — Dashboard + sub-pages
// =======================================================================
SECTIONS['klub:dashboard'] = () => {
return `
<div class="demo-banner">
<span style="font-size:18px">⬢</span>
<div><b>Klub admin view</b> — AK Kvarner Rijeka. Upravljaš članstvom, plaćanjima i dokumentima.</div>
</div>
<div class="kpi-grid">
<div class="kpi b click" onclick="navTo('clanovi')"><div class="kpi-l">Članova</div><div class="kpi-v">87</div><div class="kpi-s">aktivnih</div><span class="kpi-trend up">+4 ovaj mjesec</span></div>
<div class="kpi g click" onclick="navTo('clanarine')"><div class="kpi-l">Naplaćeno</div><div class="kpi-v">${fmtEur(2840)}</div><div class="kpi-s">članarine 2026</div></div>
<div class="kpi r click" onclick="navTo('clanarine')"><div class="kpi-l">Dug</div><div class="kpi-v">${fmtEur(420)}</div><div class="kpi-s">7 članova</div></div>
<div class="kpi a click" onclick="navTo('lijecnicki')"><div class="kpi-l">Liječnički istek</div><div class="kpi-v">3</div><div class="kpi-s">do 30 dana</div></div>
<div class="kpi c click" onclick="navTo('lijecnicki')"><div class="kpi-l">Validni liječ.</div><div class="kpi-v">82</div><div class="kpi-s">aktivnih</div></div>
<div class="kpi click" onclick="navTo('manifestacije')"><div class="kpi-l">Manifest.</div><div class="kpi-v">5</div><div class="kpi-s">nadolazeće</div></div>
</div>
<div class="row-2">
<div class="card">
<div class="card-h"><div class="card-t">👥 Najnoviji članovi</div><div class="card-actions"><button class="btn primary sm" onclick="navTo('clanovi')">Svi →</button></div></div>
${MOCK.klub_clanovi.slice(0,6).map(c => `
<div class="member-i">
<div class="av">${esc((c.ime[0]+(c.prezime?c.prezime[0]:'')).toUpperCase())}</div>
<div>
<div class="mn">${esc(c.ime+' '+c.prezime)}</div>
<div class="mp">${esc(c.kat)} · ${esc(c.discipline||'Trčanje')}</div>
</div>
<div class="mright">
${c.dug?'<span class="tag rd">Dug</span>':'<span class="tag gr">€ OK</span>'}
${c.lijecnicki==='istek'?'<span class="tag am">⚕ istek</span>':''}
</div>
</div>`).join('')}
</div>
<div class="card">
<div class="card-h"><div class="card-t">⚡ Brze akcije</div></div>
<div style="display:grid;gap:8px">
<button class="btn primary" onclick="navTo('clanovi')">+ Dodaj člana</button>
<button class="btn gold" onclick="navTo('racuni')">🧾 Skeniraj račun (OCR)</button>
<button class="btn" onclick="navTo('clanarine')">€ Članarine + HUB-3</button>
<button class="btn" onclick="navTo('lijecnicki')">⚕ Liječnički bulk ZZJZ</button>
<button class="btn" onclick="alert('Obrazac sufinanciranja — M9')">📑 Predaj zahtjev PGŽ</button>
</div>
</div>
</div>
<div class="row-2">
<div class="card">
<div class="card-h"><div class="card-t">⚕ Pregledi koji uskoro ističu</div></div>
${MOCK.klub_lijecnicki.filter(l => l.uskoro).slice(0,4).map(l => `
<div class="alert-card">
<div class="at">${esc(l.ime)}</div>
<div class="ad">Vrijedi do: <b>${esc(l.datum)}</b> · ${esc(l.dana)} dana preostaje</div>
</div>`).join('') || '<div class="empty">Svi pregledi su važeći ✓</div>'}
</div>
<div class="card">
<div class="card-h"><div class="card-t">📅 Nadolazeće manifestacije</div></div>
${MOCK.klub_manifestacije.map(m => `
<div class="alert-card ok">
<div class="at">${esc(m.naziv)}</div>
<div class="ad">${esc(m.datum)} · ${esc(m.lokacija)} · ${esc(m.tip)}</div>
</div>`).join('')}
</div>
</div>`;
};
SECTIONS['klub:clanovi'] = () => `
<div class="card"><div class="card-h"><div class="card-t">👥 Članovi AK Kvarner Rijeka (87)</div>
<div class="card-actions"><button class="btn primary">+ Dodaj člana</button></div></div>
<table><thead><tr><th>Ime</th><th>Kategorija</th><th>Disciplina</th><th>Članarina</th><th>Liječnički</th><th>Datum upisa</th></tr></thead>
<tbody>${MOCK.klub_clanovi.map(c => `<tr><td><b>${esc(c.ime+' '+c.prezime)}</b></td><td><span class="tag b">${esc(c.kat)}</span></td><td>${esc(c.discipline||'—')}</td><td>${c.dug?'<span class="tag rd">Dug</span>':'<span class="tag gr">Plaćeno</span>'}</td><td>${c.lijecnicki==='istek'?'<span class="tag am">istek</span>':'<span class="tag gr">OK</span>'}</td><td>${esc(c.upisan||'2024-09-01')}</td></tr>`).join('')}</tbody></table>
</div>`;
SECTIONS['klub:clanarine'] = () => `
<div style="margin-bottom:10px"><a href="/sport/crm" target="_blank" class="btn primary" style="text-decoration:none">📋 Otvori live CRM (HUB-3 PDF + EPC QR generator)</a></div>
<div class="row-2">
<div class="card"><div class="card-h"><div class="card-t">€ Članarine 2026</div></div>
<div class="kpi-grid"><div class="kpi g"><div class="kpi-l">Plaćeno</div><div class="kpi-v">80</div></div><div class="kpi r"><div class="kpi-l">Dug</div><div class="kpi-v">7</div></div></div>
<button class="btn primary">📧 Pošalji notifikaciju (7 dužnika)</button>
</div>
<div class="card"><div class="card-h"><div class="card-t">📄 HUB-3 uplatnica + EPC QR</div></div>
<div style="background:var(--bg3);border:1px solid var(--rim);border-radius:6px;padding:12px;font-family:var(--mono);font-size:11px">
IBAN: HR1234567890123456789<br>
Iznos: 60,00 EUR<br>
Poziv na broj: HR00 2026-{clan_id}<br>
Opis: Članarina 2026
</div>
<button class="btn gold" style="margin-top:10px">📥 Generiraj PDF</button> <button class="btn">📱 EPC QR (mobile banking)</button>
</div>
</div>
<div class="card"><div class="card-h"><div class="card-t">📋 Sve članarine</div></div>
<table><thead><tr><th>Član</th><th>Godina</th><th class="num">Iznos</th><th>Dospijeće</th><th>Datum uplate</th><th>Status</th></tr></thead>
<tbody>${MOCK.klub_clanarine.map(c => `<tr><td>${esc(c.clan)}</td><td>${esc(c.god)}</td><td class="num">${fmtEur(c.iznos)}</td><td>${esc(c.dosp)}</td><td>${esc(c.uplata||'—')}</td><td>${c.status==='OK'?'<span class="tag gr">OK</span>':'<span class="tag rd">Dug</span>'}</td></tr>`).join('')}</tbody></table>
</div>`;
SECTIONS['klub:lijecnicki'] = () => `
<div style="margin-bottom:10px"><a href="/sport/crm" target="_blank" class="btn primary" style="text-decoration:none">⚕ Otvori live CRM — pregledi + ZZJZ PGŽ scheduling</a></div>
<div class="card"><div class="card-h"><div class="card-t">⚕ Liječnički pregledi članova</div>
<div class="card-actions"><button class="btn primary">📅 Bulk ZZJZ termini</button></div></div>
<table><thead><tr><th>Član</th><th>Datum pregleda</th><th>Vrijedi do</th><th>Doktor</th><th>Status</th><th></th></tr></thead>
<tbody>${MOCK.klub_lijecnicki.map(l => `<tr><td><b>${esc(l.ime)}</b></td><td>${esc(l.pregled)}</td><td>${esc(l.datum)}</td><td>${esc(l.doktor||'Dr. Marković')}</td><td>${l.uskoro?'<span class="tag am">uskoro</span>':l.istekao?'<span class="tag rd">istekao</span>':'<span class="tag gr">OK</span>'}</td><td><button class="btn sm">PDF</button></td></tr>`).join('')}</tbody></table>
</div>`;
SECTIONS['klub:dokumenti'] = () => `
<div class="card"><div class="card-h"><div class="card-t">📄 Dokumenti kluba</div><div class="card-actions"><button class="btn primary">+ Upload</button></div></div>
<table><thead><tr><th>Naziv</th><th>Vrsta</th><th>Datum</th><th>Veličina</th><th></th></tr></thead>
<tbody>${MOCK.klub_dokumenti.map(d => `<tr><td><b>${esc(d.naziv)}</b></td><td><span class="tag b">${esc(d.vrsta)}</span></td><td>${esc(d.datum)}</td><td>${esc(d.size)}</td><td><button class="btn sm">📥</button></td></tr>`).join('')}</tbody></table>
</div>`;
SECTIONS['klub:manifestacije'] = () => `
<div class="card"><div class="card-h"><div class="card-t">📅 Manifestacije</div></div>
${MOCK.klub_manifestacije.concat([{naziv:'Kros u Krašu',datum:'2026-06-12',lokacija:'Krk',tip:'utakmica'},{naziv:'Trening kamp Platak',datum:'2026-07-04',lokacija:'Platak',tip:'priprema'}]).map(m => `
<div class="alert-card ok">
<div class="at">${esc(m.naziv)}</div>
<div class="ad">${esc(m.datum)} · ${esc(m.lokacija)} · ${esc(m.tip)}</div>
</div>`).join('')}
</div>`;
SECTIONS['klub:racuni'] = SECTIONS['pgz:racuni'];
// =======================================================================
// SPORTAŠ — Dashboard + sub-pages
// =======================================================================
SECTIONS['sportas:dashboard'] = () => `
<div class="demo-banner">
<span style="font-size:18px">👤</span>
<div><b>Sportaš view</b> — Luka Horvat. Vidiš samo svoje podatke i obrasce za potpis.</div>
</div>
<div class="row-3">
<div>
<div class="card profile-card">
<div class="profile-photo" onclick="alert('Upload nove slike profila')">LH</div>
<div class="profile-info">
<h2>Luka Horvat</h2>
<div class="sub">AK Kvarner Rijeka · Atletika · Trčanje 800m / 1500m</div>
<div class="tags-row">
<span class="tag b">Senior</span>
<span class="tag gd">Reprezentativac</span>
<span class="tag gr">Aktivan</span>
</div>
<div class="kv">
<div class="k">OIB</div><div class="v">12345678901</div>
<div class="k">Datum rođenja</div><div class="v">1998-03-14 (28 god)</div>
<div class="k">Email</div><div class="v">luka.horvat@example.hr</div>
<div class="k">Telefon</div><div class="v">+385 91 234 5678</div>
<div class="k">Trener</div><div class="v">Igor Tomić</div>
</div>
</div>
</div>
<div class="card">
<div class="card-h"><div class="card-t">🏆 Moje statistike 2026</div></div>
<div class="kpi-grid">
<div class="kpi"><div class="kpi-l">Treninzi</div><div class="kpi-v">156</div></div>
<div class="kpi b"><div class="kpi-l">Utakmice</div><div class="kpi-v">12</div></div>
<div class="kpi g"><div class="kpi-l">Pobjede</div><div class="kpi-v">8</div></div>
<div class="kpi a"><div class="kpi-l">PB 800m</div><div class="kpi-v">1:48.3</div></div>
</div>
</div>
</div>
<div>
<div class="card click-card" onclick="navTo('clanarina')">
<div class="card-h"><div class="card-t">€ Moja članarina</div><div class="card-actions"><span class="tag b">Detalji →</span></div></div>
<div class="alert-card ok">
<div class="at">2026 · Plaćeno ✓</div>
<div class="ad">Iznos: <b>60,00 €</b> · Datum uplate: 2026-01-15 · IBAN HR12...789</div>
</div>
</div>
<div class="card click-card" onclick="navTo('lijecnicki')">
<div class="card-h"><div class="card-t">⚕ Liječnički pregled</div><div class="card-actions"><span class="tag b">Detalji →</span></div></div>
<div class="alert-card">
<div class="at">⚠ Vrijedi do 2026-08-15 (103 dana)</div>
<div class="ad">Doktor: Dr. Marković · ZZJZ PGŽ</div>
</div>
</div>
<div class="card click-card" onclick="navTo('obrasci')">
<div class="card-h"><div class="card-t">📝 Obrasci za potpis</div><div class="card-actions"><span class="tag am">1 čeka</span></div></div>
<div class="alert-card crit">
<div class="at">GDPR suglasnost 2026</div>
<div class="ad">Potrebno potpisati do 2026-06-01 — klik za potpisivanje</div>
</div>
</div>
</div>
</div>`;
SECTIONS['sportas:clanarina'] = () => `
<div class="card"><div class="card-h"><div class="card-t">€ Moja članarina</div></div>
<table><thead><tr><th>Godina</th><th class="num">Iznos</th><th>Dospijeće</th><th>Uplata</th><th>Status</th><th></th></tr></thead>
<tbody>
<tr><td>2026</td><td class="num">${fmtEur(60)}</td><td>2026-01-31</td><td>2026-01-15</td><td><span class="tag gr">Plaćeno</span></td><td><button class="btn sm">PDF</button></td></tr>
<tr><td>2025</td><td class="num">${fmtEur(60)}</td><td>2025-01-31</td><td>2025-01-22</td><td><span class="tag gr">Plaćeno</span></td><td><button class="btn sm">PDF</button></td></tr>
<tr><td>2024</td><td class="num">${fmtEur(50)}</td><td>2024-01-31</td><td>2024-02-04</td><td><span class="tag gr">Plaćeno</span></td><td><button class="btn sm">PDF</button></td></tr>
</tbody></table>
</div>`;
SECTIONS['sportas:lijecnicki'] = () => `
<div style="margin-bottom:10px"><a href="/sport/crm" target="_blank" class="btn primary" style="text-decoration:none">⚕ Otvori live CRM — pregledi + ZZJZ PGŽ scheduling</a></div>`+`
<div class="card"><div class="card-h"><div class="card-t">⚕ Moji liječnički pregledi</div></div>
<div class="alert-card">
<div class="at">⚠ Trenutni: vrijedi do 2026-08-15 (103 dana)</div>
<div class="ad">Doktor: Dr. Marković · ZZJZ PGŽ Rijeka · Sportska medicina</div>
</div>
<div style="margin-top:14px;padding:14px;background:var(--bg3);border-radius:6px">
<div style="font-weight:700;color:var(--t0);margin-bottom:8px">📅 Zakazivanje preko ZZJZ PGŽ</div>
<div style="font-size:11.5px;color:var(--t2);margin-bottom:10px">Na raspolaganju imaš online termin u ZZJZ PGŽ Rijeka. Cijena pregleda: 35 €.</div>
<button class="btn primary" onclick="window.open('https://zzjzpgz.hr/','_blank')">🌐 Otvori ZZJZ PGŽ portal</button>
<button class="btn gold" style="margin-left:6px">📅 Zakaži termin</button>
</div>
<div style="margin-top:14px"><h4 style="font-size:12px;color:var(--t2);text-transform:uppercase;margin-bottom:8px">Povijest pregleda</h4>
<table><thead><tr><th>Datum</th><th>Doktor</th><th>Vrijedi do</th><th>Status</th><th></th></tr></thead>
<tbody>
<tr><td>2025-08-15</td><td>Dr. Marković</td><td>2026-08-15</td><td><span class="tag gr">aktivan</span></td><td><button class="btn sm">PDF</button></td></tr>
<tr><td>2024-08-12</td><td>Dr. Marković</td><td>2025-08-12</td><td><span class="tag">istekao</span></td><td><button class="btn sm">PDF</button></td></tr>
</tbody></table></div>
</div>`;
SECTIONS['sportas:dokumenti'] = () => `
<div class="card"><div class="card-h"><div class="card-t">📄 Moji dokumenti</div></div>
<table><thead><tr><th>Dokument</th><th>Vrsta</th><th>Datum</th><th>Status</th><th></th></tr></thead>
<tbody>
<tr><td><b>GDPR suglasnost 2026</b></td><td><span class="tag b">Pravni</span></td><td>—</td><td><span class="tag am">Treba potpis</span></td><td><button class="btn sm primary">Potpiši</button></td></tr>
<tr><td><b>Ugovor članstvo 2026</b></td><td><span class="tag b">Ugovor</span></td><td>2026-01-15</td><td><span class="tag gr">Potpisan</span></td><td><button class="btn sm">📥</button></td></tr>
<tr><td><b>Suglasnost roditelja (2024)</b></td><td><span class="tag b">Pravni</span></td><td>2024-09-01</td><td><span class="tag gr">Arhivirano</span></td><td><button class="btn sm">📥</button></td></tr>
<tr><td><b>Liječnički certifikat 2025</b></td><td><span class="tag cy">Medicinski</span></td><td>2025-08-15</td><td><span class="tag gr">Validan</span></td><td><button class="btn sm">📥</button></td></tr>
</tbody></table>
</div>`;
SECTIONS['sportas:obrasci'] = () => `
<div style="margin-bottom:10px"><a href="/sport/crm" target="_blank" class="btn primary" style="text-decoration:none">📝 Otvori live obrasce — popuni i digitalno potpiši</a></div>
<div class="card"><div class="card-h"><div class="card-t">📝 Obrasci za potpis</div></div>
<div class="alert-card crit">
<div class="at">GDPR suglasnost 2026 — obvezno do 2026-06-01</div>
<div class="ad">Klikom potpisuješ digitalno (sha256 + timestamp) — pohrana u blockchain audit (Polygon)</div>
<div style="margin-top:8px"><button class="btn primary">📝 Otvori i potpiši</button></div>
</div>
<div class="alert-card ok">
<div class="at">✓ Suglasnost na obradu osobnih podataka — POTPISANO</div>
<div class="ad">Datum: 2026-01-15 · sha256: a3f2...b9d1</div>
</div>
<div class="alert-card ok">
<div class="at">✓ Pristanak na sudjelovanje — POTPISANO</div>
<div class="ad">Datum: 2026-01-15 · sha256: 9c1d...4e8a</div>
</div>
</div>`;
SECTIONS['sportas:manifestacije'] = () => `
<div class="card"><div class="card-h"><div class="card-t">📅 Moje manifestacije / aktivnosti</div></div>
${MOCK.klub_manifestacije.map(m => `
<div class="alert-card ok">
<div class="at">${esc(m.naziv)}</div>
<div class="ad">${esc(m.datum)} · ${esc(m.lokacija)} · ${esc(m.tip)} · <b>Prijavljen ✓</b></div>
</div>`).join('')}
</div>`;
//=========== MOCK DATA ===========
const MOCK = {
zahtjevi_pending: [
{id:'Z-2026-0142', naziv:'Sufinanciranje pripremnog kampa Platak', savez:'Atletski savez PGŽ', svrha:'Pripreme za HR ligu seniora', iznos:18500, datum:'2026-04-22', status:'U obradi', klub:'AK Kvarner'},
{id:'Z-2026-0143', naziv:'Nabavka opreme — boćalište Krk', savez:'Bočarski savez PGŽ', svrha:'Renoviranje boćališta i nabavka opreme', iznos:42000, datum:'2026-04-19', status:'Čeka odluku', klub:'BK Krk'},
{id:'Z-2026-0144', naziv:'Sufinanciranje atletske staze', savez:'Atletski savez PGŽ', svrha:'Sanacija sintetičke staze Kantrida', iznos:120000, datum:'2026-04-15', status:'Pregled', klub:'—'},
{id:'Z-2026-0145', naziv:'Mladi nogometni kamp Lovran', savez:'Nogometni savez PGŽ', svrha:'Ljetne pripreme U-15 reprezentacije', iznos:28000, datum:'2026-04-10', status:'U obradi', klub:'NK Mladost'},
{id:'Z-2026-0146', naziv:'Memorijalna utrka Liburnija 2026', savez:'Atletski savez PGŽ', svrha:'Organizacija utrke + medalje', iznos:7800, datum:'2026-04-05', status:'Pregled', klub:'AK Liburnija'},
],
audit: [
{ts:'00:08:31', who:'damir@pgz.hr', what:'Odobrio zahtjev <b>Z-2026-0141</b> · 12 500 €'},
{ts:'23:54:12', who:'marija@asav.hr', what:'Predala zahtjev <b>Z-2026-0146</b> (Liburnija)'},
{ts:'23:42:08', who:'igor@kvarner.hr',what:'Dodao člana <b>Luka Horvat</b> (AK Kvarner)'},
{ts:'23:18:55', who:'damir@pgz.hr', what:'Pristup forenzika dashboardu'},
{ts:'22:51:44', who:'system', what:'Auto-scan forenzika: <b>2 nova alerta</b>'},
{ts:'22:14:17', who:'marija@asav.hr', what:'Login (2FA OK · IP 89.172.34.12)'},
],
audit_more: [
{ts:'21:02:08', who:'damir@pgz.hr', what:'Odbio zahtjev <b>Z-2026-0140</b> (nepotpuna dokumentacija)'},
{ts:'20:48:31', who:'sistema', what:'Backup DB → S3 · 1.2 GB'},
{ts:'19:55:44', who:'igor@kvarner.hr', what:'Uplata članarine: <b>Marko M.</b> · 60 €'},
{ts:'19:21:09', who:'damir@pgz.hr', what:'Kreirao novog savez admina: <b>petar@bsav.hr</b>'},
{ts:'18:33:21', who:'system', what:'Polygon seal TX <b>0xAFE9...4D2</b> · zahtjev Z-2026-0141'},
{ts:'17:50:00', who:'luka@kvarner.hr', what:'Potpisao GDPR obrazac (sha256 a3f2...b9d1)'},
],
korisnici: [
{ime:'Damir Radulić', email:'damir@pgz.hr', role:'pgz_admin', tenant:'PGŽ Odjel za sport', status:'aktivan', last_login:'00:08'},
{ime:'Marija Kovač', email:'marija@asav.hr', role:'savez_admin', tenant:'Atletski savez PGŽ', status:'aktivan', last_login:'22:14'},
{ime:'Petar Babić', email:'petar@bsav.hr', role:'savez_admin', tenant:'Bočarski savez PGŽ', status:'aktivan', last_login:'19:21'},
{ime:'Igor Tomić', email:'igor@kvarner.hr', role:'klub_admin', tenant:'AK Kvarner Rijeka', status:'aktivan', last_login:'23:42'},
{ime:'Iva Šimić', email:'iva@krk.hr', role:'klub_admin', tenant:'BK Krk', status:'aktivan', last_login:'2026-05-03'},
{ime:'Luka Horvat', email:'luka@kvarner.hr', role:'klub_clan', tenant:'AK Kvarner Rijeka', status:'aktivan', last_login:'17:50'},
{ime:'Tomislav Vrbanić', email:'tom@nsav.hr', role:'savez_admin', tenant:'Nogometni savez PGŽ',status:'aktivan', last_login:'2026-05-04'},
{ime:'Dora Pavić', email:'dora@stari.hr', role:'klub_clan', tenant:'AK Liburnija', status:'suspended', last_login:'2026-04-12'},
],
invoices: [
{datum:'2026-05-04', izdavatelj:'INA d.d.', oib:'27759560625', vrsta:'Gorivo', tag:'b', iznos:84.50, status:'Odobreno'},
{datum:'2026-05-03', izdavatelj:'HAC d.o.o.', oib:'81117323553', vrsta:'Cestarina', tag:'b', iznos:18.20, status:'Odobreno'},
{datum:'2026-05-02', izdavatelj:'Hotel Kvarner',oib:'09320229884',vrsta:'Hotel', tag:'gd', iznos:215.00,status:'Odobreno'},
{datum:'2026-05-01', izdavatelj:'Tifon d.o.o.',oib:'70289916717',vrsta:'Gorivo', tag:'b', iznos:62.40, status:'Odobreno'},
{datum:'2026-04-29', izdavatelj:'Konzum', oib:'29955634590', vrsta:'Pribor', tag:'cy', iznos:45.10, status:'U obradi'},
{datum:'2026-04-28', izdavatelj:'Restoran Trsat',oib:'82001112300',vrsta:'Dnevnice',tag:'gd', iznos:96.00, status:'Odobreno'},
],
forenzika: [
{sev:'crit',title:'PEP match — Velimir Liverić',desc:'Možda javna osoba; veza s NK X kao predsjednik. Provjeri OIB i transakcije.',tip:'PEP'},
{sev:'crit',title:'Velika gotovinska transakcija — KKK Rijeka',desc:'Iznos 12 500 € označen kao "ostalo" bez računa.',tip:'Cash'},
{sev:'warn',title:'Duplicirana isplata — Z-2026-0089',desc:'Isti iznos isplaćen dva puta unutar 24h iste organizacije.',tip:'Duplicate'},
{sev:'warn',title:'OIB ne odgovara — Sudreg',desc:'OIB u zahtjevu se ne podudara s registriranim u Sudreg.',tip:'Validation'},
],
// SAVEZ
savez_klubovi: [
{naziv:'AK Kvarner Rijeka',grad:'Rijeka',clanova:87,alert:false},
{naziv:'AK Liburnija',grad:'Opatija',clanova:54,alert:true},
{naziv:'AK Senj',grad:'Senj',clanova:32,alert:false},
{naziv:'AK Krk',grad:'Krk',clanova:28,alert:false},
{naziv:'AK Cres-Lošinj',grad:'Mali Lošinj',clanova:21,alert:false},
{naziv:'AK Crikvenica',grad:'Crikvenica',clanova:35,alert:true},
],
savez_klubovi_full: [
{naziv:'AK Kvarner Rijeka',grad:'Rijeka',clanova:87,predsjednik:'Igor Tomić',alert:false},
{naziv:'AK Liburnija',grad:'Opatija',clanova:54,predsjednik:'Marin Babić',alert:true},
{naziv:'AK Senj',grad:'Senj',clanova:32,predsjednik:'Jelena Vukšić',alert:false},
{naziv:'AK Krk',grad:'Krk',clanova:28,predsjednik:'Boris Frankopan',alert:false},
{naziv:'AK Cres-Lošinj',grad:'Mali Lošinj',clanova:21,predsjednik:'Pavao Lupis',alert:false},
{naziv:'AK Crikvenica',grad:'Crikvenica',clanova:35,predsjednik:'Ines Salopek',alert:true},
{naziv:'AK Mladost Rab',grad:'Rab',clanova:18,predsjednik:'Dario Garić',alert:false},
{naziv:'AK Vinodol',grad:'Novi Vinodolski',clanova:24,predsjednik:'Ivan Crnković',alert:false},
{naziv:'AK Klanjac',grad:'Klanjac',clanova:14,predsjednik:'Marija Tomić',alert:false},
{naziv:'AK Drenova',grad:'Rijeka',clanova:42,predsjednik:'Hrvoje Pavić',alert:false},
{naziv:'AK Marathonas',grad:'Rijeka',clanova:67,predsjednik:'Robert Šimun',alert:false},
{naziv:'AK Riječki vihor',grad:'Rijeka',clanova:65,predsjednik:'Anita Lučić',alert:true},
],
savez_zahtjevi: [
{naziv:'Sufinanciranje pripremnog kampa Platak',svrha:'HR liga seniora',status:'U obradi',tag:'am',iznos:18500,datum:'2026-04-22'},
{naziv:'Memorijalna utrka Liburnija 2026',svrha:'Organizacija utrke',status:'Pregled',tag:'b',iznos:7800,datum:'2026-04-05'},
{naziv:'Sufinanciranje atletske staze Kantrida',svrha:'Sanacija staze',status:'Pregled',tag:'b',iznos:120000,datum:'2026-04-15'},
],
savez_zahtjevi_more: [
{naziv:'Trening kamp mladi Krk 2026',svrha:'Ljetne pripreme U-15',status:'Odobreno',tag:'gr',iznos:14200,datum:'2026-03-12'},
{naziv:'Open senior atletika Rijeka',svrha:'Organizacija mitinga',status:'Odobreno',tag:'gr',iznos:9500,datum:'2026-02-28'},
{naziv:'Kros Klanjac 2025',svrha:'Tradicijska utrka',status:'Odbijeno',tag:'rd',iznos:3200,datum:'2025-09-15'},
],
savez_sportasi: [
{ime:'Luka Horvat',klub:'AK Kvarner',disciplina:'800m',kat:'Senior',lijecnicki:'ok'},
{ime:'Marko Marić',klub:'AK Kvarner',disciplina:'1500m',kat:'Senior',lijecnicki:'istek'},
{ime:'Petra Knežević',klub:'AK Liburnija',disciplina:'200m',kat:'Junior',lijecnicki:'ok'},
{ime:'Iva Tomić',klub:'AK Krk',disciplina:'Daljina',kat:'Mladi',lijecnicki:'ok'},
{ime:'Marin Crnković',klub:'AK Senj',disciplina:'Skok uvis',kat:'Senior',lijecnicki:'ok'},
{ime:'Sanja Vukšić',klub:'AK Drenova',disciplina:'400m H',kat:'Senior',lijecnicki:'istek'},
{ime:'Damir Babić',klub:'AK Marathonas',disciplina:'Maraton',kat:'Senior',lijecnicki:'ok'},
{ime:'Klara Pavić',klub:'AK Riječki vihor',disciplina:'Kugla',kat:'Junior',lijecnicki:'ok'},
],
lijecnicki_uskoro: [
{ime:'Marko Marić',klub:'AK Kvarner',datum:'2026-05-22',dana:18,doktor:'Dr. Marković'},
{ime:'Sanja Vukšić',klub:'AK Drenova',datum:'2026-05-28',dana:24,doktor:'Dr. Pavlović'},
{ime:'Iva Šimić',klub:'AK Liburnija',datum:'2026-06-02',dana:29,doktor:'Dr. Marković'},
{ime:'Tomislav Pranjić',klub:'AK Crikvenica',datum:'2026-06-04',dana:31,doktor:'Dr. Marković'},
],
// KLUB
klub_clanovi: [
{ime:'Luka', prezime:'Horvat', kat:'Senior', discipline:'800m / 1500m', dug:false, lijecnicki:'ok', upisan:'2024-09-01'},
{ime:'Marko',prezime:'Marić', kat:'Senior', discipline:'1500m', dug:false, lijecnicki:'istek', upisan:'2023-09-12'},
{ime:'Ivan', prezime:'Babić', kat:'Junior', discipline:'400m', dug:true, lijecnicki:'ok', upisan:'2025-03-04'},
{ime:'Petra',prezime:'Knežević',kat:'Junior', discipline:'200m', dug:false, lijecnicki:'ok', upisan:'2024-04-22'},
{ime:'Iva', prezime:'Tomić', kat:'Mladi', discipline:'Daljina', dug:false, lijecnicki:'ok', upisan:'2026-02-15'},
{ime:'Sara', prezime:'Lučić', kat:'Mladi', discipline:'100m', dug:true, lijecnicki:'ok', upisan:'2026-03-11'},
{ime:'Mateo',prezime:'Crnković',kat:'Senior', discipline:'Maraton', dug:false, lijecnicki:'ok', upisan:'2022-08-30'},
{ime:'Dora', prezime:'Pavić', kat:'Junior', discipline:'400m H', dug:false, lijecnicki:'istek', upisan:'2024-09-15'},
{ime:'Filip',prezime:'Šimić', kat:'Senior', discipline:'Kugla', dug:true, lijecnicki:'ok', upisan:'2023-10-01'},
{ime:'Ana', prezime:'Vrbanić', kat:'Mladi', discipline:'Daljina', dug:false, lijecnicki:'ok', upisan:'2026-01-08'},
],
klub_clanarine: [
{clan:'Luka Horvat', god:2026, iznos:60, dosp:'2026-01-31', uplata:'2026-01-15', status:'OK'},
{clan:'Marko Marić', god:2026, iznos:60, dosp:'2026-01-31', uplata:'2026-01-22', status:'OK'},
{clan:'Ivan Babić', god:2026, iznos:50, dosp:'2026-01-31', uplata:null, status:'DUG'},
{clan:'Petra Knežević',god:2026,iznos:50, dosp:'2026-01-31', uplata:'2026-02-08', status:'OK'},
{clan:'Iva Tomić', god:2026, iznos:40, dosp:'2026-02-28', uplata:'2026-02-25', status:'OK'},
{clan:'Sara Lučić', god:2026, iznos:40, dosp:'2026-03-31', uplata:null, status:'DUG'},
],
klub_lijecnicki: [
{ime:'Luka Horvat', pregled:'2025-08-15', datum:'2026-08-15', dana:103, doktor:'Dr. Marković', uskoro:false, istekao:false},
{ime:'Marko Marić', pregled:'2025-05-22', datum:'2026-05-22', dana:18, doktor:'Dr. Marković', uskoro:true, istekao:false},
{ime:'Ivan Babić', pregled:'2025-09-30', datum:'2026-09-30', dana:148, doktor:'Dr. Pavlović', uskoro:false, istekao:false},
{ime:'Petra Knežević',pregled:'2025-04-12',datum:'2026-04-12', dana:-23, doktor:'Dr. Marković', uskoro:false, istekao:true},
{ime:'Dora Pavić', pregled:'2025-04-30', datum:'2026-04-30', dana:-5, doktor:'Dr. Marković', uskoro:false, istekao:true},
{ime:'Sara Lučić', pregled:'2026-01-12', datum:'2027-01-12', dana:253, doktor:'Dr. Marković', uskoro:false, istekao:false},
],
klub_dokumenti: [
{naziv:'Statut kluba 2024', vrsta:'Statut', datum:'2024-04-15', size:'420 kB'},
{naziv:'Sudreg izvod', vrsta:'Pravni', datum:'2025-09-12', size:'180 kB'},
{naziv:'Zapisnik skupština 2026',vrsta:'Zapisnik', datum:'2026-02-22', size:'310 kB'},
{naziv:'GDPR politika', vrsta:'Pravni', datum:'2026-01-10', size:'95 kB'},
{naziv:'Ugovor sufinanciranja PGŽ 2026',vrsta:'Ugovor',datum:'2026-03-18',size:'520 kB'},
],
klub_manifestacije: [
{naziv:'Trening kamp Platak', datum:'2026-05-09', lokacija:'Platak', tip:'priprema'},
{naziv:'Liga PGŽ atletika', datum:'2026-05-11', lokacija:'Kantrida', tip:'utakmica'},
{naziv:'Open senior atletika RI', datum:'2026-05-18', lokacija:'Rijeka', tip:'natjecanje'},
{naziv:'Memorijalna utrka', datum:'2026-05-25', lokacija:'Lovran', tip:'natjecanje'},
],
};
//=========== INIT ===========
function init(){
try {
const r = localStorage.getItem('app-role');
if(r && ROLES[r]) _state.role = r;
} catch(e){}
restoreSidebar();
buildRoleSwitch();
setRole(_state.role);
}
window.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>