Files
pgz-sport/_backups/r3_cc5/app.html.r5_kalendar.1777937576
Damir Radulić f9ebcddf28 CC2 R6: middleware-wide JWT, avatar demo mode, mock mailer, login rate limit
#1 JWT middleware extended:
- Was: /api/admin/* only
- Now: any POST/PUT/PATCH/DELETE under /api/* requires Bearer JWT
- Whitelist (still anonymous): /api/auth/login, /refresh, /forgot-password,
  /password/reset, /reset-password, /setup-password, /google;
  /api/gdpr/consent; any path ending /avatar
- 14 mutating endpoints verified to return 401 without token

#2 Avatar upload demo mode (routers/clan_panel_router.py):
- Anonymous → returns {demo_mode:true, slika_url:null,
  message:'Demo mode — slika nije spremljena. Prijavite se za pravu pohranu.'},
  no FS write, no DB write
- Authenticated (valid JWT, allowed role) → real save as before
- Auth check now uses auth.auth_v2.decode_token (proper secret + revocation)
  instead of the broken local _resolve_role

#3 Mock mailer (auth/mailer.py):
- send_email writes RFC 822 .eml to /tmp/pgz_mailbox + appends to INDEX.jsonl
- send_password_reset, send_invite helpers with HR text + HTML alt
- Real SMTP active when PGZ_SMTP_HOST is set (env-driven, off by default)
- forgot-password and admin invite both call mailer; audit logs mail status

#5 Rate limiting on /api/auth/login:
- Per-user: 5 wrong attempts → 5-minute DB-backed lockout
  (was 5 → 15 min). Configurable via PGZ_LOGIN_LOCK_THRESHOLD/MINUTES.
- Per-IP: 10 fails / 5-min sliding window in-memory → HTTP 429
  Configurable via PGZ_LOGIN_IP_THRESHOLD/WINDOW_SEC. Successful
  login clears the IP counter.
- Failed attempts respond '(N/5) — račun je zaključan na 5 minuta'
- New audit actions: login.ratelimit.ip; login.fail meta now
  includes fails count, locked, lock_minutes

#4 Live test report: 46/46 across 6 demo users — login, JWT gate on 14
   mutating endpoints, public path whitelist, demo-mode avatar +
   real save, forgot-password e-mail to mailbox, no-leak unknown email,
   5-fail lockout, 423 during lockout, audit coverage.
2026-05-05 01:42:53 +02:00

1855 lines
112 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="hr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<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}
.sb.collapsed .nav-sep{font-size:0;padding:6px 0;text-align:center;border-top:1px dashed var(--rim);margin:6px 8px 4px}
.sb.collapsed .nav-ext span:last-child{display:none}
.nav-ext{color:var(--cyan)}
.nav-ext:hover{color:var(--pgz-gold);background:var(--bg2)}
.nav-ext.active{background:linear-gradient(90deg,var(--pgz-blue) 0%,var(--pgz-blue2) 100%);color:#fff}
.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);cursor:pointer;padding:4px 8px;border-radius:6px;transition:all .15s}
.tb-user:hover{background:var(--bg2)}
.tb-user .av{width:32px;height:32px;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:12px;overflow:hidden;flex-shrink:0;border:2px solid transparent}
.tb-user:hover .av{border-color:var(--pgz-gold)}
.tb-user .av img{width:100%;height:100%;object-fit:cover}
.tb-user .role-badge{font-size:9px;background:var(--pgz-gold);color:var(--bg0);padding:1px 5px;border-radius:3px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;margin-left:4px}
.tb-user .tenant-name{font-size:10px;color:var(--t4)}
/* Drill-down right panel (shared) */
#dpanel{position:fixed;top:0;right:-720px;width:680px;max-width:96vw;height:100vh;background:var(--bg1);border-left:1px solid var(--rim);z-index:200;transition:right .25s ease;display:flex;flex-direction:column;box-shadow:-8px 0 30px rgba(0,0,0,.5)}
#dpanel.open{right:0}
#dpanel-hdr{padding:14px 18px;border-bottom:1px solid var(--rim);display:flex;align-items:center;justify-content:space-between;flex-shrink:0;background:var(--bg2);gap:10px}
#dpanel-t{font-size:14px;font-weight:700;color:var(--t0)}
#dpanel-x{cursor:pointer;font-size:22px;color:var(--t4);width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:5px;transition:all .15s}
#dpanel-x:hover{background:var(--bg3);color:var(--red)}
#dpanel-body{flex:1;overflow-y:auto;padding:16px}
#dpanel-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:199;backdrop-filter:blur(2px)}
#dpanel-overlay.open{display:block}
/* Profile page styles */
.profile-page{max-width:980px;margin:0 auto}
.profile-banner{display:flex;align-items:center;gap:18px;padding:22px;background:linear-gradient(135deg,var(--pgz-blue) 0%,var(--bg2) 60%);border:1px solid var(--rim);border-radius:10px;margin-bottom:16px;position:relative;overflow:hidden}
.profile-banner::before{content:"";position:absolute;top:0;right:0;width:200px;height:100%;background:radial-gradient(circle at 100% 0%,rgba(244,196,48,.18) 0%,transparent 60%);pointer-events:none}
.profile-avatar-big{width:96px;height:96px;border-radius:50%;background:linear-gradient(135deg,var(--pgz-blue2),var(--pgz-gold));color:#fff;font-weight:800;font-size:32px;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:3px solid var(--pgz-gold);overflow:hidden;position:relative;cursor:pointer}
.profile-avatar-big img{width:100%;height:100%;object-fit:cover}
.profile-avatar-big .upload-hint{position:absolute;inset:0;background:rgba(0,0,0,.55);color:#fff;font-size:10.5px;font-weight:700;display:flex;align-items:center;justify-content:center;text-align:center;padding:6px;opacity:0;transition:opacity .15s}
.profile-avatar-big:hover .upload-hint{opacity:1}
.profile-banner-info h1{font-size:22px;color:#fff;margin-bottom:4px;font-weight:800}
.profile-banner-info .role-line{font-size:11.5px;color:var(--t1);margin-bottom:6px}
.profile-banner-info .tags-row .tag{margin-right:4px}
.profile-banner-actions{margin-left:auto;display:flex;gap:8px;flex-shrink:0;z-index:1}
.profile-section{background:var(--bg2);border:1px solid var(--rim);border-radius:8px;padding:16px;margin-bottom:14px}
.profile-section h3{font-size:12px;font-weight:700;color:var(--pgz-gold);text-transform:uppercase;letter-spacing:1px;margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--rim);display:flex;align-items:center;justify-content:space-between}
.profile-section .edit-link{font-size:11px;color:var(--cyan);cursor:pointer;text-transform:none;letter-spacing:0;font-weight:600}
.profile-section .edit-link:hover{color:var(--pgz-gold)}
.profile-row{display:grid;grid-template-columns:160px 1fr auto;gap:8px 14px;padding:8px 0;border-bottom:1px dashed var(--rim);align-items:center}
.profile-row:last-child{border:0}
.profile-row .k{color:var(--t2);font-size:11.5px;font-weight:600}
.profile-row .v{color:var(--t1);font-size:12.5px;word-break:break-word}
.profile-row .v.empty{color:var(--t4);font-style:italic}
.profile-row input,.profile-row select{background:var(--bg3);border:1px solid var(--rim);border-radius:5px;padding:6px 10px;color:var(--t1);font-size:12.5px;width:100%}
.profile-row .a{display:flex;gap:4px}
.profile-row .a button{padding:4px 8px;font-size:11px}
.tag-2fa-on{background:var(--green);color:var(--bg0);padding:2px 7px;border-radius:3px;font-size:10px;font-weight:700;text-transform:uppercase}
.tag-2fa-off{background:var(--rim2);color:var(--t1);padding:2px 7px;border-radius:3px;font-size:10px;font-weight:700;text-transform:uppercase}
.tag-gdpr{background:var(--cyan);color:var(--bg0);padding:2px 7px;border-radius:3px;font-size:10px;font-weight:700;text-transform:uppercase}
.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" id="tb-user" onclick="navTo('profil')" title="Otvori moj profil">
<div class="av" id="user-av">DR</div>
<div>
<div style="font-weight:700" id="user-name">Damir Radulić<span class="role-badge" id="user-role-badge">pgz admin</span></div>
<div class="tenant-name" id="user-tenant">Primorsko-goranska županija</div>
</div>
</div>
</div>
</div>
<div class="content" id="content">
<div class="loading">Učitavanje...</div>
</div>
</main>
</div>
<!-- Drill-down right panel -->
<div id="dpanel-overlay" onclick="closeDetail()"></div>
<aside id="dpanel" aria-hidden="true">
<div id="dpanel-hdr">
<div id="dpanel-t">Detalji</div>
<div id="dpanel-x" onclick="closeDetail()" title="Zatvori (Esc)">×</div>
</div>
<div id="dpanel-body"><div class="loading">Učitavanje...</div></div>
</aside>
<input type="file" id="avatar-input" accept="image/jpeg,image/png,image/webp" style="display:none" onchange="onAvatarPick(this)">
<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; }
}
// JWT-aware fetch wrapper
function getToken(){ try { return localStorage.getItem('jwt') || localStorage.getItem('access_token') || ''; } catch(e){ return ''; } }
async function apiAuth(path, opts){
opts = opts || {};
const h = Object.assign({}, opts.headers || {});
const tok = getToken(); if(tok) h['Authorization'] = 'Bearer '+tok;
if(opts.body && !(opts.body instanceof FormData) && !h['Content-Type']) h['Content-Type'] = 'application/json';
try {
const r = await fetch(API+path, Object.assign({}, opts, {headers:h}));
if(r.status === 401){ return {__unauthorized:true, status:401}; }
if(!r.ok) return {__error:true, status:r.status};
if(r.headers.get('content-type')?.includes('application/json')) return await r.json();
return {__ok:true};
} catch(e){ return {__error:true, msg:String(e)}; }
}
const initials = (n) => { if(!n) return '?'; const p=String(n).trim().split(/\s+/); return ((p[0]||'')[0]||'')+((p[1]||'')[0]||'').toUpperCase(); };
//=========== 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:'profil', ic:'\u{1F464}', label:'Moj profil'},
{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:'kalendar', ic:'\u{1F4C5}', label:'Kalendar'},
{id:'audit', ic:'\u{1F50D}', label:'Audit log'},
{id:'forenzika', ic:'⚠', label:'Forenzika', badge:11},
],
savez: [
{id:'profil', ic:'\u{1F464}', label:'Moj profil'},
{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:'profil', ic:'\u{1F464}', label:'Moj profil'},
{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:'kalendar', ic:'\u{1F4C5}', label:'Kalendar'},
{id:'manifestacije', ic:'\u{1F4C5}', label:'Manifestacije'},
{id:'racuni', ic:'\u{1F9FE}', label:'Računi (OCR)'},
],
sportas: [
{id:'profil', ic:'\u{1F464}', label:'Moj profil'},
{id:'dashboard', ic:'\u{1F4CA}', label:'Dashboard'},
{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:'kalendar', ic:'\u{1F4C5}', label:'Kalendar'},
{id:'manifestacije', ic:'\u{1F4C5}', label:'Manifestacije'},
],
};
const _state = {role:'pgz', section:'dashboard', me:null, demoMode:true};
// Map server user_type -> UI role bucket (for nav layout)
function userTypeToRole(t){
const m = {
super_admin:'pgz', pgz_admin:'pgz', pgz_viewer:'pgz',
savez_admin:'savez',
klub_admin:'klub', klub_trener:'klub',
klub_clan:'sportas', sportas:'sportas', viewer:'pgz'
};
return m[t] || 'pgz';
}
// Try real auth first; fall back to demo mode
async function loadCurrentUser(){
if(!getToken()) return null;
const me = await apiAuth('/auth/me');
if(!me || me.__unauthorized || me.__error){
if(me && me.__unauthorized){ try { localStorage.removeItem('jwt'); } catch(e){} }
return null;
}
_state.me = me;
_state.demoMode = false;
_state.role = userTypeToRole(me.user_type);
return me;
}
function applyMeToHeader(){
const me = _state.me; if(!me) return;
const name = me.full_name || ((me.ime||'')+' '+(me.prezime||'')).trim() || me.email || '—';
const tenant = me.tenant_name || (me.tenant_type ? me.tenant_type.toUpperCase() : '');
const roleLabel = (ROLES[_state.role]||{}).name || me.user_type || 'Korisnik';
// Topbar
$('#user-name').innerHTML = esc(name) + `<span class="role-badge" id="user-role-badge">${esc(me.user_type||'')}</span>`;
$('#user-tenant').textContent = tenant;
$('#user-role-label')?.replaceChildren(document.createTextNode(roleLabel));
// Avatar topbar
if(me.avatar_url){
$('#user-av').innerHTML = `<img src="${esc(me.avatar_url)}" alt="">`;
} else if(me.google_picture){
$('#user-av').innerHTML = `<img src="${esc(me.google_picture)}" alt="">`;
} else {
$('#user-av').textContent = initials(name);
}
// Sidebar footer
if($('#sf-name')) $('#sf-name').textContent = name;
if($('#sf-role')) $('#sf-role').textContent = roleLabel;
if($('#sf-av')){
if(me.avatar_url) $('#sf-av').innerHTML = `<img src="${esc(me.avatar_url)}" alt="" style="width:100%;height:100%;object-fit:cover;border-radius:50%">`;
else if(me.google_picture) $('#sf-av').innerHTML = `<img src="${esc(me.google_picture)}" alt="" style="width:100%;height:100%;object-fit:cover;border-radius:50%">`;
else $('#sf-av').textContent = initials(name);
}
if($('#role-sub')) $('#role-sub').textContent = tenant || roleLabel;
}
//=========== DRILL-DOWN PANEL ===========
function openDetail(title, html){
$('#dpanel-t').textContent = title || 'Detalji';
$('#dpanel-body').innerHTML = html || '<div class="empty">Nema sadržaja.</div>';
$('#dpanel').classList.add('open');
$('#dpanel-overlay').classList.add('open');
$('#dpanel').setAttribute('aria-hidden','false');
}
function closeDetail(){
$('#dpanel').classList.remove('open');
$('#dpanel-overlay').classList.remove('open');
$('#dpanel').setAttribute('aria-hidden','true');
}
document.addEventListener('keydown', e => { if(e.key==='Escape') closeDetail(); });
async function showDetail(kind, id, title){
openDetail(title || kind, '<div class="loading">Učitavam detalje...</div>');
let body = '';
try {
if(kind === 'savez'){
const d = await api('/savezi/'+id);
if(!d){ body = '<div class="empty">Savez nije pronađen.</div>'; }
else {
body = `
<h2 style="font-size:18px;color:var(--t0);margin-bottom:6px">${esc(d.naziv||'—')}</h2>
<div style="font-size:11px;color:var(--t2);margin-bottom:14px">${esc(d.skraceni_naziv||'')} · ${esc(d.oib||'')}</div>
<div class="kv">
<div class="k">Predsjednik</div><div class="v">${esc(d.predsjednik||'—')}</div>
<div class="k">Tajnik</div><div class="v">${esc(d.tajnik||'—')}</div>
<div class="k">Email</div><div class="v">${esc(d.email||'—')}</div>
<div class="k">Telefon</div><div class="v">${esc(d.telefon||'—')}</div>
<div class="k">Adresa</div><div class="v">${esc(d.adresa||'—')}</div>
<div class="k">Web</div><div class="v">${d.web?`<a href="${esc(d.web)}" target="_blank">${esc(d.web)}</a>`:'—'}</div>
<div class="k">Klubova</div><div class="v">${fmt(d.broj_klubova||'—')}</div>
<div class="k">Sportaša</div><div class="v">${fmt(d.broj_sportasa||'—')}</div>
<div class="k">Godina osnutka</div><div class="v">${esc(d.godina_osnutka||'—')}</div>
</div>
<div style="margin-top:14px"><a href="/sport/?savez=${id}" target="_blank" class="btn primary">Otvori u javnom portalu →</a></div>`;
}
} else if(kind === 'klub'){
const d = await api('/klubovi/'+id);
if(!d){ body = '<div class="empty">Klub nije pronađen.</div>'; }
else body = `
<h2 style="font-size:18px;color:var(--t0);margin-bottom:6px">${esc(d.naziv||'—')}</h2>
<div style="font-size:11px;color:var(--t2);margin-bottom:14px">${esc(d.savez||'')} · ${esc(d.grad||'')}</div>
<div class="kv">
<div class="k">OIB</div><div class="v">${esc(d.oib||'—')}</div>
<div class="k">Predsjednik</div><div class="v">${esc(d.predsjednik||'—')}</div>
<div class="k">Adresa</div><div class="v">${esc(d.adresa||'—')}</div>
<div class="k">Email</div><div class="v">${esc(d.email||'—')}</div>
<div class="k">Telefon</div><div class="v">${esc(d.telefon||'—')}</div>
<div class="k">Članova</div><div class="v">${fmt(d.broj_clanova||'—')}</div>
</div>`;
} else if(kind === 'zahtjev'){
const z = MOCK.zahtjevi_pending.concat(MOCK.savez_zahtjevi||[]).find(x => x.id===id || x.naziv===id) || {};
body = `
<h2 style="font-size:17px;color:var(--t0);margin-bottom:6px">${esc(z.naziv||id)}</h2>
<div class="kv">
<div class="k">Šifra</div><div class="v">${esc(z.id||'—')}</div>
<div class="k">Savez</div><div class="v">${esc(z.savez||'—')}</div>
<div class="k">Klub</div><div class="v">${esc(z.klub||'—')}</div>
<div class="k">Svrha</div><div class="v">${esc(z.svrha||'—')}</div>
<div class="k">Iznos</div><div class="v"><b style="color:var(--pgz-gold);font-size:15px">${fmtEur(z.iznos)}</b></div>
<div class="k">Datum predaje</div><div class="v">${esc(z.datum||'—')}</div>
<div class="k">Status</div><div class="v"><span class="tag am">${esc(z.status||'—')}</span></div>
</div>
<div style="margin-top:16px;display:flex;gap:8px">
<button class="btn primary">✓ Odobri</button>
<button class="btn">↩ Vrati podnositelju</button>
<button class="btn">✗ Odbij</button>
</div>
<div style="margin-top:18px;padding:14px;background:var(--bg3);border-radius:6px">
<div style="font-weight:700;color:var(--pgz-gold);font-size:11px;text-transform:uppercase;margin-bottom:8px">🔗 Blockchain seal</div>
<div style="font-size:11px;color:var(--t2)">Po odobrenju, hash zahtjeva + iznos zapisuje se u Polygon PoS (M11). Wallet: 0xD874...d368</div>
</div>`;
} else if(kind === 'audit'){
const a = MOCK.audit.concat(MOCK.audit_more||[]).find(x => x.what===id) || {ts:'',who:'',what:id};
body = `
<div class="kv">
<div class="k">Vrijeme</div><div class="v">${esc(a.ts)}</div>
<div class="k">Korisnik</div><div class="v" style="color:var(--pgz-gold)">${esc(a.who)}</div>
<div class="k">Akcija</div><div class="v">${a.what}</div>
</div>`;
} else if(kind === 'lijecnicki'){
body = `<div class="kv">
<div class="k">Sportaš</div><div class="v">${esc(id)}</div>
<div class="k">ZZJZ PGŽ</div><div class="v"><a href="https://zzjzpgz.hr" target="_blank">zzjzpgz.hr</a></div>
</div>
<div style="margin-top:14px"><button class="btn primary">📅 Zakaži termin (ZZJZ)</button></div>`;
} else if(kind === 'clan'){
body = `<h3 style="color:var(--t0);margin-bottom:10px">${esc(id)}</h3>
<div class="empty">Detalji člana — production: dohvati iz /api/clanovi/{id}</div>`;
} else {
body = '<div class="empty">Detalji.</div>';
}
} catch(e){ body = '<div class="empty">Greška pri dohvaćanju: '+esc(String(e))+'</div>'; }
$('#dpanel-body').innerHTML = body;
}
//=========== 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 = 'profil';
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];
// In demo mode, populate header from ROLES table; in real-auth mode, applyMeToHeader() owns it
if(_state.demoMode){
$('#user-name').innerHTML = esc(role.user) + `<span class="role-badge">${esc(role.name)}</span>`;
$('#user-av').innerHTML = '';
$('#user-av').textContent = role.av;
$('#user-tenant').textContent = role.sub;
$('#sf-name').textContent = role.user;
$('#sf-role').textContent = role.name;
$('#sf-av').innerHTML = '';
$('#sf-av').textContent = role.av;
}
$('#role-sub').textContent = (_state.me?.tenant_name) || role.sub;
$('#role-section-label').textContent = role.name.toUpperCase();
buildNav();
navTo('profil');
}
//=========== NAV ===========
const NAV_EXTERNAL = [
{id:'login', href:'/sport/login', ic:'\u{1F511}', label:'Prijava'},
{id:'app', href:'/sport/app', ic:'\u{1F4F1}', label:'Aplikacija'},
{id:'admin', href:'/sport/admin', ic:'\u{1F6E1}', label:'Administracija'},
{id:'crm', href:'/sport/crm', ic:'\u{1F465}', label:'CRM'},
{id:'erp', href:'/sport/erp', ic:'\u{1F4B0}', label:'ERP'},
{id:'kpi', href:'/sport/kpi', ic:'\u{1F4C8}', label:'KPI'},
{id:'audit', href:'/sport/audit', ic:'\u{1F4CB}', label:'Audit'},
{id:'sport2', href:'/sport/static/sport2.html', ic:'\u{1F310}', label:'Public portal'}
];
function buildNav(){
const items = NAV_BY_ROLE[_state.role] || [];
let html = 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('');
// PORTALI separator + external links (active = 'app')
html += '<div class="nav-sep" style="padding:14px 14px 4px 14px;font-size:9.5px;color:var(--t4);text-transform:uppercase;letter-spacing:1.2px;font-weight:700;white-space:nowrap;overflow:hidden">Portali</div>';
html += NAV_EXTERNAL.map(n =>
`<a class="nav-i nav-ext ${n.id==='app'?'active':''}" href="${n.href}" data-id="${n.id}" data-label="${esc(n.label)}" style="text-decoration:none">
<span class="ic">${n.ic}</span><span class="lbl">${esc(n.label)}</span>
<span style="margin-left:auto;font-size:10px;opacity:.5">↗</span>
</a>`
).join('');
$('#nav').innerHTML = html;
}
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: {
profil:['Moj profil','Osobni podaci i postavke'],
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'],
kalendar:['Kalendar','Liječnički termini, manifestacije, eventi'],
audit:['Audit log','Sve aktivnosti sustava'],
forenzika:['Forenzika','Sumnjive transakcije / PEP'],
},
savez: {
profil:['Moj profil','Osobni podaci'],
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: {
profil:['Moj profil','Osobni podaci'],
dashboard:['Dashboard','AK Kvarner Rijeka'],
clanovi:['Članovi','Članovi kluba'],
clanarine:['Članarine','Stanje članarina'],
lijecnicki:['Liječnički','Pregledi članova'],
dokumenti:['Dokumenti','Dokumenti kluba'],
kalendar:['Kalendar','Liječnički termini + manifestacije'],
manifestacije:['Manifestacije','Nadolazeće aktivnosti'],
racuni:['Računi','Troškovi kluba'],
},
sportas: {
profil:['Moj profil','Osobni podaci'],
dashboard:['Pregled','Moja aktivnost'],
clanarina:['Članarina','Stanje moje članarine'],
lijecnicki:['Liječnički','Moj liječnički pregled'],
dokumenti:['Moji dokumenti','Suglasnosti, ugovori'],
obrasci:['Obrasci','Za potpis'],
kalendar:['Kalendar','Moji termini i događaji'],
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 = {};
// ──────────────────────── PROFILE PAGE (shared by all roles) ────────────────────────
function profileMe(){
// Real user if available, else demo from ROLES table
if(_state.me) return _state.me;
const r = ROLES[_state.role] || ROLES.pgz;
const parts = String(r.user||'').split(/\s+/);
return {
id: 0, email:'demo@pgz.hr',
full_name: r.user, ime: parts[0]||'', prezime: parts.slice(1).join(' '),
user_type: _state.role==='pgz'?'pgz_admin':_state.role==='savez'?'savez_admin':_state.role==='klub'?'klub_admin':'klub_clan',
tenant_type: _state.role==='pgz'?'pgz':_state.role,
tenant_name: r.sub, tenant_id: null, tier: _state.role==='pgz'?0:_state.role==='savez'?1:2,
oib: '12345678901', telefon:'+385 91 234 5678', phone:null,
last_login: '2026-05-05T00:08:09', preferred_language:'hr',
avatar_url:null, two_factor_enabled:false,
gdpr_consent_at:null, created_at:'2026-04-01T08:00:00',
roles:[{code:'demo', naziv:r.name}]
};
}
function profileRender(){
const u = profileMe();
const name = u.full_name || ((u.ime||'')+' '+(u.prezime||'')).trim() || u.email || '—';
const av = u.avatar_url ? `<img src="${esc(u.avatar_url)}" alt="">`
: (u.google_picture ? `<img src="${esc(u.google_picture)}" alt="">` : esc(initials(name)));
const lastLogin = u.last_login ? new Date(u.last_login).toLocaleString('hr-HR') : '—';
const created = u.created_at ? new Date(u.created_at).toLocaleString('hr-HR') : '—';
const gdpr = u.gdpr_consent_at ? new Date(u.gdpr_consent_at).toLocaleDateString('hr-HR') : null;
const roleLabel = (ROLES[_state.role]||{}).name || u.user_type || 'Korisnik';
return `
<div class="profile-page">
<div class="profile-banner">
<div class="profile-avatar-big" id="prof-av-big" onclick="pickAvatar()" title="Klik za upload nove slike">
${av}
<div class="upload-hint">📷 Promijeni sliku</div>
</div>
<div class="profile-banner-info">
<h1>${esc(name)}</h1>
<div class="role-line">${esc(roleLabel)} · ${esc(u.tenant_name || u.tenant_type || '')}</div>
<div class="tags-row">
<span class="tag b">${esc(u.user_type||'')}</span>
${u.aktivan!==false ? '<span class="tag gr">Aktivan</span>' : '<span class="tag rd">Suspended</span>'}
${u.two_factor_enabled ? '<span class="tag-2fa-on">2FA ON</span>' : '<span class="tag-2fa-off">2FA OFF</span>'}
${gdpr ? `<span class="tag-gdpr">GDPR ${esc(gdpr)}</span>` : ''}
</div>
</div>
<div class="profile-banner-actions">
<button class="btn" onclick="pickAvatar()">📷 Slika</button>
<button class="btn primary" onclick="profileEditAll()">✏ Uredi profil</button>
</div>
</div>
<div class="profile-section">
<h3>Osobni podaci <span class="edit-link" onclick="profileEditAll()">✏ Uredi sva polja</span></h3>
<div class="profile-row" data-f="ime">
<div class="k">Ime</div>
<div class="v">${esc(u.ime||'')||'<span class="empty">—</span>'}</div>
<div class="a"><button class="btn sm" onclick="profileEditField('ime','Ime')">✏</button></div>
</div>
<div class="profile-row" data-f="prezime">
<div class="k">Prezime</div>
<div class="v">${esc(u.prezime||'')||'<span class="empty">—</span>'}</div>
<div class="a"><button class="btn sm" onclick="profileEditField('prezime','Prezime')">✏</button></div>
</div>
<div class="profile-row" data-f="full_name">
<div class="k">Puno ime</div>
<div class="v">${esc(u.full_name||'')||'<span class="empty">—</span>'}</div>
<div class="a"><button class="btn sm" onclick="profileEditField('full_name','Puno ime')">✏</button></div>
</div>
<div class="profile-row" data-f="email">
<div class="k">Email</div>
<div class="v">${esc(u.email||'—')}</div>
<div class="a"><span class="tag">read-only</span></div>
</div>
<div class="profile-row" data-f="telefon">
<div class="k">Telefon</div>
<div class="v">${esc(u.telefon||u.phone||'')||'<span class="empty">—</span>'}</div>
<div class="a"><button class="btn sm" onclick="profileEditField('telefon','Telefon')">✏</button></div>
</div>
<div class="profile-row" data-f="oib">
<div class="k">OIB</div>
<div class="v">${esc(u.oib||'')||'<span class="empty">—</span>'}</div>
<div class="a"><button class="btn sm" onclick="profileEditField('oib','OIB')">✏</button></div>
</div>
<div class="profile-row" data-f="preferred_language">
<div class="k">Jezik sučelja</div>
<div class="v">${esc(u.preferred_language||'hr')}</div>
<div class="a"><button class="btn sm" onclick="profileEditField('preferred_language','Jezik (hr/en)')">✏</button></div>
</div>
</div>
<div class="profile-section">
<h3>Tenant i ovlasti</h3>
<div class="profile-row"><div class="k">Tenant</div><div class="v">${esc(u.tenant_name || '—')}</div><div class="a"></div></div>
<div class="profile-row"><div class="k">Tip tenanta</div><div class="v"><span class="tag b">${esc(u.tenant_type || '—')}</span></div><div class="a"></div></div>
<div class="profile-row"><div class="k">Tier</div><div class="v">${u.tier!=null?u.tier:'—'} ${u.tier===0?'(PGŽ)':u.tier===1?'(savez)':u.tier===2?'(klub)':''}</div><div class="a"></div></div>
<div class="profile-row"><div class="k">User type</div><div class="v"><span class="tag gd">${esc(u.user_type || '—')}</span></div><div class="a"></div></div>
<div class="profile-row"><div class="k">Dodatne uloge</div><div class="v">${(u.roles||[]).map(r => `<span class="tag b" style="margin-right:4px">${esc(r.code)}</span>`).join('')||'<span class="empty">—</span>'}</div><div class="a"></div></div>
</div>
<div class="profile-section">
<h3>Sigurnost <span class="edit-link" onclick="profileChangePassword()">🔑 Promijeni lozinku</span></h3>
<div class="profile-row"><div class="k">2FA</div><div class="v">${u.two_factor_enabled?'<span class="tag-2fa-on">Uključeno</span>':'<span class="tag-2fa-off">Nije postavljeno</span>'}</div><div class="a"><button class="btn sm primary" onclick="profileSetup2FA()">${u.two_factor_enabled?'Provjeri':'Postavi'}</button></div></div>
<div class="profile-row"><div class="k">Mora promijeniti lozinku</div><div class="v">${u.must_change_pwd?'<span class="tag rd">DA</span>':'<span class="tag gr">NE</span>'}</div><div class="a"></div></div>
<div class="profile-row"><div class="k">GDPR pristanak</div><div class="v">${gdpr || '<span class="empty">Nije zabilježen</span>'}</div><div class="a">${gdpr?'':'<button class="btn sm">Dodijeli</button>'}</div></div>
<div class="profile-row"><div class="k">Status računa</div><div class="v"><span class="tag ${u.aktivan===false?'rd':'gr'}">${u.aktivan===false?'Suspended':'Aktivan'}</span></div><div class="a"></div></div>
</div>
<div class="profile-section">
<h3>Aktivnost</h3>
<div class="profile-row"><div class="k">Zadnji login</div><div class="v" style="font-family:var(--mono);font-size:11.5px">${esc(lastLogin)}</div><div class="a"></div></div>
<div class="profile-row"><div class="k">Račun kreiran</div><div class="v" style="font-family:var(--mono);font-size:11.5px">${esc(created)}</div><div class="a"></div></div>
<div class="profile-row"><div class="k">User ID</div><div class="v" style="font-family:var(--mono)">#${esc(u.id||0)}</div><div class="a"></div></div>
</div>
<div class="profile-section">
<h3>GDPR i podaci</h3>
<div style="font-size:11.5px;color:var(--t2);line-height:1.6;margin-bottom:10px">
Imaš pravo na pristup, izmjenu i brisanje svojih osobnih podataka prema GDPR uredbi (čl. 1517, 20).
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn" onclick="alert('Izvoz JSON svih podataka — backend M10')">📤 Izvezi moje podatke (JSON)</button>
<button class="btn" onclick="alert('Pregled audit zapisa o pristupu — M10')">🔍 Audit pristupa mojim podacima</button>
<button class="btn" style="border-color:var(--red);color:var(--red)" onclick="profileDeleteAccount()">🗑 Zatraži brisanje računa</button>
</div>
</div>
</div>`;
}
SECTIONS['pgz:profil'] = profileRender;
SECTIONS['savez:profil'] = profileRender;
SECTIONS['klub:profil'] = profileRender;
SECTIONS['sportas:profil']= profileRender;
// Profile actions
function pickAvatar(){
if(!getToken()){
alert('Avatar upload zahtijeva login (JWT). U demo modu nije dostupan.');
return;
}
$('#avatar-input').click();
}
async function onAvatarPick(input){
const f = input.files && input.files[0];
if(!f) return;
if(f.size > 5*1024*1024){ alert('Slika prevelika (>5 MB)'); return; }
const fd = new FormData(); fd.append('file', f);
const av = $('#prof-av-big');
if(av) av.innerHTML = '<div style="font-size:14px;color:var(--t1)">⏳</div>';
const r = await apiAuth('/auth/me/avatar', {method:'POST', body:fd});
input.value = '';
if(r && r.avatar_url){
if(_state.me) _state.me.avatar_url = r.avatar_url;
applyMeToHeader();
loadSection(); // re-render profile
} else {
alert('Upload failed: '+(r&&r.status||'unknown'));
loadSection();
}
}
async function profileEditField(field, label){
const cur = (_state.me && _state.me[field]) || '';
const v = prompt(`${label}:`, cur);
if(v == null) return;
if(!getToken()){
if(_state.me){ _state.me[field] = v; }
else {
// demo: persist on local copy
if(!window._demoMe) window._demoMe = profileMe();
window._demoMe[field] = v;
_state.me = window._demoMe;
}
applyMeToHeader();
loadSection();
return;
}
const r = await apiAuth('/auth/me', {method:'PUT', body: JSON.stringify({[field]: v})});
if(r && !r.__error){ _state.me = r; applyMeToHeader(); loadSection(); }
else alert('Greška pri spremanju: '+(r&&r.status||'unknown'));
}
async function profileEditAll(){
// Open drill-down panel with full edit form
const u = profileMe();
openDetail('Uredi profil', `
<form id="prof-edit-form" onsubmit="return profileSaveAll(event)">
${['ime','prezime','full_name','telefon','oib'].map(f => `
<div style="margin-bottom:12px">
<label style="display:block;font-size:11px;color:var(--t2);margin-bottom:4px;font-weight:600;text-transform:uppercase">${f}</label>
<input name="${f}" value="${esc(u[f]||'')}" style="background:var(--bg3);border:1px solid var(--rim);border-radius:5px;padding:8px 10px;color:var(--t1);font-size:13px;width:100%">
</div>`).join('')}
<div style="margin-bottom:12px">
<label style="display:block;font-size:11px;color:var(--t2);margin-bottom:4px;font-weight:600;text-transform:uppercase">Jezik</label>
<select name="preferred_language" style="background:var(--bg3);border:1px solid var(--rim);border-radius:5px;padding:8px 10px;color:var(--t1);font-size:13px;width:100%">
<option value="hr" ${(u.preferred_language||'hr')==='hr'?'selected':''}>Hrvatski</option>
<option value="en" ${u.preferred_language==='en'?'selected':''}>English</option>
</select>
</div>
<div style="display:flex;gap:8px;margin-top:18px">
<button type="submit" class="btn primary">💾 Spremi</button>
<button type="button" class="btn" onclick="closeDetail()">Odustani</button>
</div>
</form>`);
}
async function profileSaveAll(ev){
ev.preventDefault();
const fd = new FormData(ev.target);
const obj = {}; fd.forEach((v,k) => { obj[k]=v; });
if(!getToken()){
Object.assign(_state.me || {}, obj);
applyMeToHeader(); closeDetail(); loadSection();
return false;
}
const r = await apiAuth('/auth/me', {method:'PUT', body: JSON.stringify(obj)});
if(r && !r.__error){ _state.me = r; applyMeToHeader(); closeDetail(); loadSection(); }
else alert('Greška: '+(r&&r.status||'unknown'));
return false;
}
async function profileChangePassword(){
if(!getToken()){ alert('Login potreban (demo mode).'); return; }
const oldp = prompt('Stara lozinka:'); if(oldp==null) return;
const newp = prompt('Nova lozinka (min 8 znakova):'); if(newp==null) return;
if(newp.length < 8){ alert('Nova lozinka mora imati barem 8 znakova'); return; }
const r = await apiAuth('/auth/password/change', {method:'POST', body: JSON.stringify({old_password:oldp,new_password:newp})});
if(r && r.status==='ok') alert('Lozinka promijenjena ✓'); else alert('Greška: '+(r&&r.status||'unknown'));
}
async function profileSetup2FA(){
if(!getToken()){ alert('Login potreban (demo mode).'); return; }
const r = await apiAuth('/auth/2fa/setup', {method:'POST'});
if(r && r.qr_url) {
openDetail('Postavi 2FA', `<div style="text-align:center"><img src="${esc(r.qr_url)}" style="max-width:240px"><div style="margin-top:10px;font-size:12px;color:var(--t2)">Skeniraj QR kod u Google Authenticator / Authy</div><div style="margin-top:14px"><input id="totp-code" placeholder="6-cifreni kod" style="background:var(--bg3);border:1px solid var(--rim);border-radius:5px;padding:8px;color:var(--t1);width:140px"><button class="btn primary" onclick="profileVerify2FA()">Potvrdi</button></div></div>`);
} else alert('2FA setup failed');
}
async function profileVerify2FA(){
const code = $('#totp-code')?.value;
const r = await apiAuth('/auth/2fa/verify', {method:'POST', body: JSON.stringify({code})});
if(r && r.status==='ok'){ alert('2FA aktivirano ✓'); closeDetail(); loadSection(); }
else alert('Pogrešan kod.');
}
function profileDeleteAccount(){
if(!confirm('Zaista zatraži brisanje računa? GDPR brisanje je nepovratno.')) return;
alert('Zahtjev za brisanje poslan na PGŽ admin (M10 — backend).');
}
// =======================================================================
// 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="showDetail('zahtjev','${esc(z.id)}','Zahtjev ${esc(z.id)}')">
<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" style="cursor:pointer" onclick='showDetail("audit",${JSON.stringify(a.what)},"Audit zapis")'><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 style="cursor:pointer" onclick="showDetail('savez',${s.id},${JSON.stringify(s.naziv)})">
<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="event.stopPropagation();showDetail('savez',${s.id},${JSON.stringify(s.naziv)})">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 style="cursor:pointer" onclick="showDetail('klub',${k.id},${JSON.stringify(k.naziv)})">
<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 + Ri.NET AI Engine 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>`;
// =======================================================================
// CC5 R5 — KALENDAR (liječnički termini + manifestacije + eventi)
// =======================================================================
async function renderKalendar(opts){
opts = opts || {};
const today = new Date();
const ym = opts.ym || (today.getFullYear() + '-' + String(today.getMonth()+1).padStart(2,'0'));
const [Y, M] = ym.split('-').map(Number);
const first = new Date(Y, M-1, 1);
const last = new Date(Y, M, 0);
// Učitaj sve liječničke koji ističu unutar +180 dana, manifestacije iz API-ja, i mock eventove
let lij = [], manif = [], notif = [];
try { const d = await fetch('/sport/api/crm/lijecnicki/uskoro-isticu?days=180&include_expired=false').then(r=>r.json()); lij = d.rows || []; } catch(e){}
try { const d = await fetch('/sport/api/manifestacije').then(r=>r.json()); manif = d.rows || d || []; } catch(e){}
try { const d = await fetch('/sport/api/crm/notifications?limit=50').then(r=>r.json()); notif = d.rows || []; } catch(e){}
const events = [];
lij.forEach(l => events.push({date: l.vrijedi_do, type:'lij', title:`⚕ Pregled ističe: ${l.clan}`, klub:l.klub, color:'a'}));
manif.forEach(m => { if (m.datum) events.push({date: m.datum, type:'manif', title:`📅 ${m.naziv || m.title || 'Manifestacija'}`, klub:m.lokacija || m.grad, color:'b'}); });
// Eventi: ZZJZ termini mock — sljedećih 7 dana po radnim danima
for(let d=0; d<14; d++){
const dt = new Date(); dt.setDate(dt.getDate()+d);
if (dt.getDay()===0 || dt.getDay()===6) continue;
if ((dt.getDate() + d) % 5 === 0) {
events.push({date: dt.toISOString().slice(0,10), type:'event', title:'🏥 ZZJZ termin slot (mock)', color:'g'});
}
}
// KPI / sažetak
const cntLij = lij.length, cntManif = manif.length, cntNotif = notif.filter(n=>!n.read_at && n.channel==='inapp').length;
// Group events by date
const byDate = {};
events.forEach(e => {
if (!e.date) return;
const k = String(e.date).substring(0,10);
(byDate[k] = byDate[k] || []).push(e);
});
// Header s navigacijom
const prevYm = (() => { const d = new Date(Y, M-2, 1); return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0'); })();
const nextYm = (() => { const d = new Date(Y, M, 1); return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0'); })();
const monthName = first.toLocaleString('hr-HR', {month:'long', year:'numeric'});
// Build kalendar grid (start ponedjeljak)
let firstDow = first.getDay(); if (firstDow === 0) firstDow = 7; // pon=1, ned=7
const blanks = firstDow - 1;
const days = last.getDate();
let grid = '';
const dayNames = ['Pon','Uto','Sri','Čet','Pet','Sub','Ned'];
grid += `<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:6px">`;
dayNames.forEach(d => grid += `<div style="font-size:11px;color:var(--t3);text-align:center;font-weight:600;text-transform:uppercase;padding:4px 0">${d}</div>`);
for(let i=0; i<blanks; i++) grid += `<div></div>`;
for(let d=1; d<=days; d++){
const k = `${Y}-${String(M).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
const ev = byDate[k] || [];
const isToday = (k === today.toISOString().slice(0,10));
const evHtml = ev.slice(0,3).map(e => `<div style="font-size:10px;background:rgba(${e.color==='a'?'245,158,11':e.color==='b'?'26,115,232':'34,197,94'},0.18);padding:2px 4px;border-radius:3px;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(e.title)}${e.klub?' — '+esc(e.klub):''}">${esc(e.title.substring(0,28))}</div>`).join('');
const more = ev.length > 3 ? `<div style="font-size:9px;color:var(--t3);margin-top:2px">+${ev.length-3} više</div>` : '';
grid += `<div style="background:${isToday?'rgba(26,115,232,0.15)':'var(--bg2)'};border:1px solid ${isToday?'var(--pgz-blue)':'var(--rim)'};border-radius:6px;padding:6px;min-height:90px;${ev.length?'cursor:pointer':''}" ${ev.length?`onclick="alert('${esc(ev.map(x=>x.title+(x.klub?' — '+x.klub:'')).join('\\n').replace(/'/g,'\\\\\\'')\)}')"`:''}><div style="font-weight:600;font-size:13px;color:${isToday?'var(--pgz-blue)':'var(--t1)'}">${d}</div>${evHtml}${more}</div>`;
}
grid += '</div>';
// Lista nadolazećih (top 10)
const upcoming = events.filter(e => e.date && e.date >= today.toISOString().slice(0,10))
.sort((a,b) => a.date.localeCompare(b.date)).slice(0, 10);
const upcomingHtml = upcoming.map(e => `<tr><td>${esc(e.date)}</td><td>${esc(e.title)}</td><td>${esc(e.klub||'—')}</td><td><span class="tag ${e.color==='a'?'am':e.color==='b'?'bl':'gr'}">${e.type}</span></td></tr>`).join('');
return `
<div class="kpi-grid" style="margin-bottom:12px">
<div class="kpi a"><div class="kpi-l">⚕ Liječnički isteci</div><div class="kpi-v">${cntLij}</div><div class="kpi-s">≤ 180 dana</div></div>
<div class="kpi b"><div class="kpi-l">📅 Manifestacije</div><div class="kpi-v">${cntManif}</div></div>
<div class="kpi r"><div class="kpi-l">🔔 InApp neprocitano</div><div class="kpi-v">${cntNotif}</div></div>
<div class="kpi g"><div class="kpi-l">Eventa u kalendaru</div><div class="kpi-v">${events.length}</div></div>
</div>
<div class="card">
<div class="card-h">
<div class="card-t">📅 ${esc(monthName)}</div>
<div class="card-actions" style="display:flex;gap:6px;align-items:center">
<button class="btn sm" onclick="$('#content').innerHTML='<div class=loading>Učitavanje...</div>';renderKalendar({ym:'${prevYm}'}).then(h=>$('#content').innerHTML=h)">←</button>
<input type="month" value="${ym}" onchange="$('#content').innerHTML='<div class=loading>...</div>';renderKalendar({ym:this.value}).then(h=>$('#content').innerHTML=h)" style="background:var(--bg2);border:1px solid var(--rim);color:var(--t1);padding:4px 8px;border-radius:4px">
<button class="btn sm" onclick="$('#content').innerHTML='<div class=loading>Učitavanje...</div>';renderKalendar({ym:'${nextYm}'}).then(h=>$('#content').innerHTML=h)">→</button>
<button class="btn primary sm" onclick="fetch('/sport/api/crm/lijecnicki/notify-scan',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'}).then(r=>r.json()).then(d=>alert('Skenirano: '+d.created+' notifikacija kreirano'))">🔔 Scan isteke → notifikacije</button>
</div>
</div>
<div style="padding:14px">${grid}</div>
</div>
<div class="card">
<div class="card-h"><div class="card-t">📋 Nadolazeći eventi (10)</div></div>
<table>
<thead><tr><th>Datum</th><th>Naziv</th><th>Lokacija/Klub</th><th>Tip</th></tr></thead>
<tbody>${upcomingHtml || '<tr><td colspan="4" class="empty">Nema nadolazećih eventa.</td></tr>'}</tbody>
</table>
</div>
<div class="card">
<div class="card-h"><div class="card-t">🔔 Aktivne InApp notifikacije (10)</div>
<div class="card-actions"><button class="btn sm" onclick="fetch('/sport/api/crm/notifications/mark-all-read',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({channel:'inapp'})}).then(r=>r.json()).then(d=>{alert('Označeno '+d.marked_read+' kao pročitano');loadSection();})">Označi sve pročitano</button></div>
</div>
<div style="padding:8px 14px">
${notif.filter(n=>!n.read_at && n.channel==='inapp').slice(0,10).map(n => `
<div style="display:flex;gap:10px;align-items:start;padding:8px 0;border-bottom:1px solid var(--rim)">
<div style="font-size:18px">${n.subject.includes('ISTEKAO')?'⚠':'⚕'}</div>
<div style="flex:1">
<div style="font-weight:600;font-size:13px">${esc(n.subject)}</div>
<div style="font-size:11px;color:var(--t3);margin-top:2px">${esc((n.body||'').substring(0,140))}…</div>
</div>
<button class="btn sm" onclick="fetch('/sport/api/crm/notifications/${n.id}/read',{method:'POST'}).then(()=>loadSection())">✓</button>
</div>`).join('') || '<div class="empty">Nema neprocitanih notifikacija. Pokreni "Scan isteke" da generiraš nove.</div>'}
</div>
</div>
`;
}
SECTIONS['pgz:kalendar'] = renderKalendar;
SECTIONS['savez:kalendar'] = renderKalendar;
SECTIONS['klub:kalendar'] = renderKalendar;
SECTIONS['sportas:kalendar'] = renderKalendar;
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 ===========
async function init(){
try {
const r = localStorage.getItem('app-role');
if(r && ROLES[r]) _state.role = r;
} catch(e){}
restoreSidebar();
buildRoleSwitch();
// Try real auth (JWT)
const me = await loadCurrentUser();
if(me){
// Real-auth mode — hide demo role switcher (only super_admin can switch personas)
if(me.user_type !== 'super_admin'){
const rs = $('#role-switch'); if(rs) rs.style.display='none';
}
applyMeToHeader();
}
// First page after login: Moj profil
setRole(_state.role);
navTo('profil');
}
window.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>