Files
pgz-sport/static/app.html
T
damir ae9c4e2bfd Sportski objekti: API + Leaflet map page + address enrichment
DB: pgz_sport.sportski_objekti (103 objekti, 103 s geo, 60 s adresom, 31 tip)

API:
- /api/v2/sportski-objekti (filter: tip, grad, sport, q)
- /api/v2/sportski-objekti/meta (tipovi, gradovi, sportovi, ukupno)

Frontend:
- /static/objekti.html — Leaflet (OpenStreetMap) interactive map
- 3 dropdown filter (tip, grad, sport) + search
- Side panel s listom + map markers s ikonama (🏟️🏊🎿🎳⛸️🎯🥌🏃)
- Popup: naziv, tip, kapacitet, adresa, upravitelj, izgradeno, sportovi, web link, Google Maps link
- /objekti, /sport/objekti, /sport/api/v2/sportski-objekti routes

Sidebar app.html: +Sportski objekti link
Background: scripts/objekti_enrich_address.py (Nominatim reverse-geocode 60 objekata bez adrese)
2026-05-05 18:35:04 +02:00

2503 lines
141 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="hr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>PGŽ SPORT — Operativna aplikacija</title>
<!--
app.html v1.0 — Round 3 M4
PGŽ Sport operational app — 4 role dashboards (PGŽ admin, savez admin, klub admin, sportaš)
Author: dradulic@outlook.com / damir@rinet.one — 2026-05-05
Same CSS variables / theme as sport2.html. Sidebar is collapsible (M3 logic).
-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
<style>
:root{
--pgz-blue:#003087; --pgz-blue2:#004CC4; --pgz-gold:#F4C430;
--bg0:#08090e; --bg1:#0d1021; --bg2:#111628; --bg3:#161d35; --bg4:#1c2542;
--rim:#1e2a50; --rim2:#283560;
--t0:#fff; --t1:#e2e6f0; --t2:#8a95b4; --t4:#4e5a7a;
--green:#00e88f; --red:#ff2d55; --amber:#f59e0b; --cyan:#00c8e8;
--font:'Inter',sans-serif; --mono:'JetBrains Mono',monospace;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{font-family:var(--font);background:var(--bg0);color:var(--t1);font-size:13px;overflow-x:hidden}
a{color:var(--cyan);text-decoration:none}
a:hover{color:var(--pgz-gold)}
button,input,select,textarea{font-family:inherit;font-size:inherit;outline:none}
::-webkit-scrollbar{width:8px;height:8px}
::-webkit-scrollbar-track{background:var(--bg1)}
::-webkit-scrollbar-thumb{background:var(--rim2);border-radius:4px}
::-webkit-scrollbar-thumb:hover{background:var(--pgz-blue2)}
/* ============ LAYOUT ============ */
.app{display:flex;min-height:100vh}
/* Native .sb hidden — shared sidebar (/static/shared/sidebar.*) handles sectioned menu */
.sb{display:none}
.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:0;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}
}
/* === MOBILE RESPONSIVE (CRISIS FIX) === */
@media (max-width: 768px) {
body { font-size: 14px; }
.app { display: block !important; }
.sb {
position: fixed; left: -260px; top: 0; width: 260px; height: 100vh;
z-index: 1000; transition: left 0.3s ease;
}
.sb.mobile-open { left: 0; }
.main { margin-left: 0 !important; padding: 12px !important; }
.topbar { padding: 8px 12px !important; }
.topbar #user-tenant { display: none; }
#user-name { font-size: 12px !important; }
.role-badge { font-size: 9px !important; padding: 2px 6px !important; }
/* Mobile menu hamburger */
.mobile-menu-btn {
display: inline-flex !important; padding: 8px; cursor: pointer;
background: var(--bg2); border: 1px solid var(--rim); border-radius: 4px;
font-size: 18px; color: var(--t1); margin-right: 8px;
}
/* Drill-down panel full-width on mobile */
#dpanel { width: 100vw !important; max-width: 100vw !important; right: -100vw !important; }
#dpanel.open { right: 0 !important; }
/* Profile responsive */
.profile-page { padding: 8px !important; }
.kv { grid-template-columns: 1fr !important; }
/* Tables horizontal scroll */
table { font-size: 12px !important; }
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
/* KPI grid */
.kpi-grid { grid-template-columns: 1fr 1fr !important; gap: 8px !important; }
/* Buttons full-width on mobile in forms */
form .btn { width: 100%; margin-top: 8px; }
}
@media (min-width: 769px) {
.mobile-menu-btn { display: none !important; }
}
/* Sidebar overlay backdrop on mobile when open */
.sb-backdrop {
display: none; position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 999;
}
@media (max-width: 768px) {
.sb-backdrop.show { display: block; }
}
/* Hamburger button visibility (CRISIS V3) */
.mobile-menu-btn {
display: none;
background: var(--bg2,#1a1a1e);
color: var(--t1,#fff);
border: 1px solid var(--rim,#2a2a2e);
padding: 6px 10px;
font-size: 18px;
border-radius: 4px;
cursor: pointer;
margin-right: 8px;
}
@media (max-width: 768px) {
.mobile-menu-btn { display: inline-flex !important; align-items: center; justify-content: center; }
.sb {
position: fixed !important; left: -280px !important; top: 0 !important;
width: 260px !important; height: 100vh !important; z-index: 1000 !important;
transition: left 0.3s ease !important;
}
.sb.mobile-open { left: 0 !important; }
.main { margin-left: 0 !important; }
.sb-backdrop {
display: none; position: fixed; inset: 0;
background: rgba(0,0,0,.55); z-index: 999;
}
.sb-backdrop.show { display: block !important; }
/* Center mobile content */
.content, .main { padding: 12px !important; }
.tb { padding: 8px 12px !important; }
}
</style>
<link rel="stylesheet" href="/static/shared/sidebar.css">
<script src="/static/shared/sidebar.js" defer data-active="profil"></script>
<script src="/static/oib_format.js" defer></script>
</head>
<body>
<div class="app">
<aside class="sb" id="sb">
<div class="sb-h">
<a href="/" class="logo" style="text-decoration:none;color:inherit;cursor:pointer" title="Početna"><span style="font-weight:800;letter-spacing:.5px">PGŽ</span> <span class="g">SPORT</span></a>
<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">
<button class="mobile-menu-btn" onclick="toggleMobileSidebar()" aria-label="Menu" type="button"></button>
<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; }
}
// ─── Sport-aware enrichment source (cached) ───
const _appEnrichSrc = {};
async function appOpenEnrichSource(sport, naziv){
if(!sport){ alert('Sport nije naveden — ne mogu odrediti savez.'); return; }
const k = (sport||'').toLowerCase();
let src = _appEnrichSrc[k];
if(!src){
const d = await api('/v2/enrich-sources?sport='+encodeURIComponent(sport));
src = d && d.match;
if(src) _appEnrichSrc[k] = src;
}
if(!src){ alert('Nema definiranog izvora za sport: '+sport); return; }
const base = (src.base_url||'').replace(/\/$/,'');
const url = ((src.sport||'').toLowerCase()==='nogomet')
? base + '/klubovi?q=' + encodeURIComponent(naziv||'')
: base + '/?s=' + encodeURIComponent(naziv||'');
window.open(url, '_blank', 'noopener');
}
// JWT-aware fetch wrapper
function getToken(){
try {
return localStorage.getItem('pgz_access')
|| sessionStorage.getItem('pgz_access')
|| 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();
// ━━━ JWT EXPIRY PRE-CHECK ━━━
if(tok){
try{
const payload = JSON.parse(atob(tok.split('.')[1]));
if(payload.exp && payload.exp * 1000 < Date.now()){
console.warn('[apiAuth] JWT expired client-side, redirecting');
['pgz_access','pgz_refresh','pgz_user','jwt','access_token'].forEach(k => {
try{localStorage.removeItem(k); sessionStorage.removeItem(k);}catch(e){}
});
if(!window.__pgz_redirecting){ window.__pgz_redirecting = true; window.location.href = '/login?reason=expired'; }
return {__unauthorized:true, status:401};
}
}catch(e){ /* token not parseable, continue and let server respond */ }
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){
// ━━━ GLOBAL 401 HANDLER — clear + redirect ━━━
console.warn('[apiAuth] 401 from server, clearing localStorage + redirecting');
['pgz_access','pgz_refresh','pgz_user','jwt','access_token'].forEach(k => {
try{localStorage.removeItem(k); sessionStorage.removeItem(k);}catch(e){}
});
// Don't redirect from /login itself; allow profile page to handle
const onLogin = location.pathname.includes('/login');
if(!onLogin && !window.__pgz_redirecting){
window.__pgz_redirecting = true;
window.(window.__pgz_made_api_call ? location.href = '/login?reason=unauthorized' : console.warn('[auth] no token but no API call yet, skipping redirect'));
}
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', href:'/admin/users'},
{id:'savezi', ic:'\u{1F3C5}', label:'Savezi'},
{id:'objekti', ic:'\u{1F3DF}', label:'Sportski objekti', href:'/objekti'},
{id:'klubovi', ic:'⬢', label:'Klubovi'},
{id:'sportasi', ic:'\u{1F464}', label:'Sportaši'},
{id:'financije', ic:'€', label:'Financije'},
{id:'erp', ic:'\u{1F4BC}', label:'ERP', href:'/erp/full'},
{id:'crm', ic:'\u{1F4DD}', label:'CRM', href:'/crm/v2'},
{id:'dokumenti', ic:'\u{1F4D6}', label:'Dokumenti'},
{id:'racuni', ic:'\u{1F9FE}', label:'Računi (OCR)', href:'/erp/full?tab=uploads'},
{id:'putni', ic:'\u{2708}', label:'Putni nalozi', href:'/erp/full?tab=putni'},
{id:'kalendar', ic:'\u{1F4C5}', label:'Kalendar'},
{id:'notif', ic:'\u{1F514}', label:'Notifikacije'},
{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:'notif', ic:'\u{1F514}', label:'Notifikacije'},
{id:'lijecnicki',ic:'⚕', label:'Liječnički'},
{id:'racuni', ic:'\u{1F9FE}', label:'Računi (OCR)', href:'/erp/full?tab=uploads'},
{id:'putni', ic:'\u{2708}', label:'Putni nalozi', href:'/erp/full?tab=putni'},
],
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:'notif', ic:'\u{1F514}', label:'Notifikacije'},
{id:'manifestacije', ic:'\u{1F4C5}', label:'Manifestacije'},
{id:'racuni', ic:'\u{1F9FE}', label:'Računi (OCR)', href:'/erp/full?tab=uploads'},
{id:'putni', ic:'\u{2708}', label:'Putni nalozi', href:'/erp/full?tab=putni'},
],
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:'notif', ic:'\u{1F514}', label:'Notifikacije'},
{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)}${me.avatar_url.includes('?')?'&':'?'}t=${Date.now()}" 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)}${me.avatar_url.includes('?')?'&':'?'}t=${Date.now()}" 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?formatOib(d.oib,{savez_id:d.id}):'')}</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.sport||'')} · ${esc(d.grad||'')}</div>
<div class="kv">
<div class="k">OIB</div><div class="v">${d.oib?esc(formatOib(d.oib,{klub_id:d.id,savez_id:d.savez_id})):'—'}</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>
<div style="margin-top:14px;display:flex;gap:8px;flex-wrap:wrap">
<button class="btn" onclick="appOpenEnrichSource(${JSON.stringify(d.sport||'')}, ${JSON.stringify(d.naziv||'')})">🌐 Obogati podatke (sport-savez)</button>
</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 ===========
function buildNav(){
const items = NAV_BY_ROLE[_state.role] || [];
$('#nav').innerHTML = items.map((n, idx) => {
const click = n.href
? `onclick="window.location.href='${n.href}'"`
: `onclick="navTo('${n.id}')"`;
return `<div class="nav-i ${n.id===_state.section?'active':''}" data-id="${n.id}" data-label="${esc(n.label)}" ${click}>
<span class="ic">${n.ic}</span>
<span class="lbl">${esc(n.label)}</span>
${n.badge?`<span class="badge">${n.badge}</span>`:''}
</div>`;
}).join('');
}
window.addEventListener('hashchange', () => {
const h = (location.hash||'').replace(/^#/,'');
if(!h) return;
const items = NAV_BY_ROLE[_state.role] || [];
if(items.some(n => n.id===h)) navTo(h);
});
function navTo(id){
_state.section = id;
$$('.nav-i').forEach(el => el.classList.toggle('active', el.dataset.id===id));
loadSection();
}
async function logout(){
if(!confirm('Odjava iz aplikacije?')) return;
// Call backend to revoke JWT
try{
const tok = getToken();
if(tok){
await fetch(API+'/auth/logout', {
method:'POST',
headers:{'Authorization':'Bearer '+tok}
}).catch(()=>{});
}
}catch(e){}
// Clear ALL session keys (not just demo placeholders)
['pgz_access','pgz_refresh','pgz_user','app-role','jwt','access_token','refresh_token','pgz_session_id'].forEach(k => {
try{localStorage.removeItem(k); sessionStorage.removeItem(k);}catch(e){}
});
window.location.href = '/login';
}
//=========== 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 (ERP Full)'],
putni:['Putni nalozi','Otvara ERP Full → Putni nalozi'],
crm:['CRM','Članarine + liječnički'],
kalendar:['Kalendar','Liječnički termini, manifestacije, eventi'],
notif:['Notifikacije','Centar obavijesti — InApp poruke'],
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'],
notif:['Notifikacije','Centar obavijesti — InApp poruke'],
lijecnicki:['Liječnički','Pregledi članova saveza'],
racuni:['Računi','Računi saveza (ERP Full)'],
putni:['Putni nalozi','ERP Full → Putni nalozi'],
},
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'],
notif:['Notifikacije','Centar obavijesti — InApp poruke'],
manifestacije:['Manifestacije','Nadolazeće aktivnosti'],
racuni:['Računi','Troškovi kluba (ERP Full)'],
putni:['Putni nalozi','ERP Full → Putni nalozi'],
},
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'],
notif:['Notifikacije','Centar obavijesti — InApp poruke'],
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="gdprExport()">📤 Izvezi moje podatke (JSON)</button>
<button class="btn" onclick="gdprAuditMy()">🔍 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('Niste prijavljeni. Idite na /login pa se prijavite kao damir@pgz.hr / PGZ2026!');
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;
// Update localStorage so other pages (sport2.html footer, sidebar) see new avatar
try{
const stored = localStorage.getItem('pgz_user') || sessionStorage.getItem('pgz_user');
if(stored){
const u = JSON.parse(stored);
u.avatar_url = r.avatar_url;
if(localStorage.getItem('pgz_user')) localStorage.setItem('pgz_user', JSON.stringify(u));
else sessionStorage.setItem('pgz_user', JSON.stringify(u));
}
}catch(e){console.warn('avatar storage update failed', e);}
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.');
}
// profileDeleteAccount: real implementation below (line ~1902)
// =======================================================================
// 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="window.location.href='/erp/full?tab=uploads'">🧾 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 onPGZ = !!window._pgz_filter_priority;
const url = onPGZ ? '/v2/savezi/priority-sort?only=true&limit=300' : '/savezi';
const d = await api(url) || {rows:[]};
const top = (d.rows||[]).slice(0,30);
const bp = window.pgzBadgePrefix || (()=> '');
const rows = top.map(s => `
<tr style="cursor:pointer" onclick="showDetail('savez',${s.id},${JSON.stringify(s.naziv)})">
<td><b>${bp(s)}${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('');
const tb = window.renderPGZToggleBtn ? window.renderPGZToggleBtn() : '';
setTimeout(()=>{ const b=document.getElementById('app-exp-savezi'); if(b && window.attachExportDropdown) window.attachExportDropdown(b, ()=>'/sport/api'+url, 'savezi'); },0);
return `<div class="card"><div class="card-h"><div class="card-t">🏅 Savezi PGŽ — top 30 (od ${d.count||246})${onPGZ?' · ⭐ samo PGŽ-relevantni':''}</div><button id="app-exp-savezi" class="export-btn" type="button">Export ▾</button></div>
${tb}
<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 onPGZ = !!window._pgz_filter_priority;
const url = onPGZ ? '/klubovi?kategorija=priority&limit=80' : '/klubovi?limit=40';
const d = await api(url) || {rows:[]};
const bp = window.pgzBadgePrefix || (()=> '');
const rows = (d.rows||[]).slice(0,80).map(k => `
<tr style="cursor:pointer" onclick="showDetail('klub',${k.id},${JSON.stringify(k.naziv||k.klub)})">
<td><b>${bp(k)}${esc(k.naziv||k.klub||'—')}</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('');
const tb = window.renderPGZToggleBtn ? window.renderPGZToggleBtn() : '';
setTimeout(()=>{ const b=document.getElementById('app-exp-klubovi'); if(b && window.attachExportDropdown) window.attachExportDropdown(b, ()=>'/sport/api'+url, 'klubovi'); },0);
return `<div class="card"><div class="card-h"><div class="card-t">⬢ Klubovi (${d.count||0})${onPGZ?' · ⭐ samo PGŽ-prioritet':''}</div><button id="app-exp-klubovi" class="export-btn" type="button">Export ▾</button></div>
${tb}
<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 onPGZ = !!window._pgz_filter_priority;
const url = onPGZ ? '/v2/clanovi/priority-sort?only=true&limit=400' : '/clanovi?limit=40';
const d = await api(url) || {rows:[]};
const bp = window.pgzBadgePrefix || (()=> '');
const rows = (d.rows||[]).slice(0,80).map(c => `
<tr>
<td><b>${bp(c)}${esc((c.ime||'')+' '+(c.prezime||''))}</b></td>
<td>${esc(c.klub||c.klub_naziv||c.klub_naziv_godisnjak||'—')}</td>
<td>${esc(c.kategorija||'—')}</td>
<td>${esc(c.spol||'—')}</td>
<td>${esc(c.datum_rodjenja||c.datum_rodenja||'—')}</td>
</tr>`).join('');
const tb = window.renderPGZToggleBtn ? window.renderPGZToggleBtn() : '';
setTimeout(()=>{ const b=document.getElementById('app-exp-sportasi'); if(b && window.attachExportDropdown) window.attachExportDropdown(b, ()=>'/sport/api'+url, 'sportasi'); },0);
return `<div class="card"><div class="card-h"><div class="card-t">👤 Sportaši (${d.count||0})${onPGZ?' · ⭐ samo PGŽ-prioritet':''}</div><button id="app-exp-sportasi" class="export-btn" type="button">Export ▾</button></div>
${tb}
<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>`;
};
// Računi (OCR) je premješten u ERP Full → tab Uploads (consolidation 2026-05-05).
// Legacy entry preusmjerava korisnika.
SECTIONS['pgz:racuni'] = () => {
setTimeout(() => { window.location.href = '/erp/full?tab=uploads'; }, 50);
return `<div class="card"><div class="card-h"><div class="card-t">🧾 Računi (OCR) — premješteno u ERP Full</div></div>
<p style="color:var(--t2);margin:8px 0">Otvaranje <b>ERP Full → Uploads (OCR)</b>…</p>
<a class="btn primary" href="/erp/full?tab=uploads" style="text-decoration:none">📎 Otvori sada</a>
</div>`;
};
SECTIONS['pgz:putni'] = () => {
setTimeout(() => { window.location.href = '/erp/full?tab=putni'; }, 50);
return `<div class="card"><div class="card-h"><div class="card-t">✈ Putni nalozi — premješteno u ERP Full</div></div>
<p style="color:var(--t2);margin:8px 0">Otvaranje <b>ERP Full → Putni nalozi</b>…</p>
<a class="btn primary" href="/erp/full?tab=putni" style="text-decoration:none">✈ Otvori sada</a>
</div>`;
};
SECTIONS['pgz:crm'] = () => `
<div style="margin-bottom:12px">
<a href="/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)
// Agent B 2026-05-05: Added CRUD on pgz_sport.kalendar_events
// =======================================================================
// ─── KALENDAR CRUD helpers ─────────────────────────────────────────────
window._kalState = window._kalState || { events: [], ym: null };
async function kalLoadEvents(ym){
const [Y, M] = ym.split('-').map(Number);
const from = `${Y}-${String(M).padStart(2,'0')}-01`;
const nm = M===12 ? {y:Y+1,m:1} : {y:Y,m:M+1};
const to = `${nm.y}-${String(nm.m).padStart(2,'0')}-01`;
try {
const d = await apiAuth(`/v2/kalendar/events?from=${from}&to=${to}`);
return (d && d.rows) || [];
} catch(e){ return []; }
}
function _kalFmtLocal(iso){
if(!iso) return '';
const d = new Date(iso);
if(isNaN(d.getTime())) return '';
const pad = n => String(n).padStart(2,'0');
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
function kalEventModalHtml(ev){
ev = ev || {};
const isEdit = !!ev.id;
return `
<div id="kalModalBg" style="position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:9999;display:flex;align-items:center;justify-content:center" onclick="if(event.target===this) kalCloseModal()">
<div style="background:var(--bg2);border:1px solid var(--rim);border-radius:8px;width:min(560px,92vw);max-height:90vh;overflow:auto">
<div style="padding:14px 16px;border-bottom:1px solid var(--rim);display:flex;justify-content:space-between;align-items:center">
<div style="font-weight:600;font-size:15px">${isEdit?'Uredi termin #'+ev.id:'Novi termin'}</div>
<button class="btn sm" onclick="kalCloseModal()">✕</button>
</div>
<div style="padding:14px 16px;display:flex;flex-direction:column;gap:10px">
<label style="display:flex;flex-direction:column;gap:4px;font-size:11px;color:var(--t3);text-transform:uppercase;font-weight:600">
Naziv *
<input id="kalF_title" value="${esc(ev.title||'')}" style="background:var(--bg1);border:1px solid var(--rim);color:var(--t1);padding:8px;border-radius:4px;font-size:13px">
</label>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<label style="display:flex;flex-direction:column;gap:4px;font-size:11px;color:var(--t3);text-transform:uppercase;font-weight:600">
Početak *
<input id="kalF_start" type="datetime-local" value="${_kalFmtLocal(ev.start_at)}" style="background:var(--bg1);border:1px solid var(--rim);color:var(--t1);padding:8px;border-radius:4px;font-size:13px">
</label>
<label style="display:flex;flex-direction:column;gap:4px;font-size:11px;color:var(--t3);text-transform:uppercase;font-weight:600">
Kraj
<input id="kalF_end" type="datetime-local" value="${_kalFmtLocal(ev.end_at)}" style="background:var(--bg1);border:1px solid var(--rim);color:var(--t1);padding:8px;border-radius:4px;font-size:13px">
</label>
</div>
<label style="display:flex;flex-direction:column;gap:4px;font-size:11px;color:var(--t3);text-transform:uppercase;font-weight:600">
Lokacija
<input id="kalF_loc" value="${esc(ev.location||'')}" style="background:var(--bg1);border:1px solid var(--rim);color:var(--t1);padding:8px;border-radius:4px;font-size:13px">
</label>
<label style="display:flex;flex-direction:column;gap:4px;font-size:11px;color:var(--t3);text-transform:uppercase;font-weight:600">
Opis
<textarea id="kalF_desc" rows="3" style="background:var(--bg1);border:1px solid var(--rim);color:var(--t1);padding:8px;border-radius:4px;font-size:13px;resize:vertical">${esc(ev.description||'')}</textarea>
</label>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<label style="display:flex;flex-direction:column;gap:4px;font-size:11px;color:var(--t3);text-transform:uppercase;font-weight:600">
Tip
<select id="kalF_type" style="background:var(--bg1);border:1px solid var(--rim);color:var(--t1);padding:8px;border-radius:4px;font-size:13px">
${['event','meeting','manif','training','medical','other'].map(t => `<option value="${t}" ${ev.event_type===t?'selected':''}>${t}</option>`).join('')}
</select>
</label>
<label style="display:flex;flex-direction:column;gap:4px;font-size:11px;color:var(--t3);text-transform:uppercase;font-weight:600">
Boja
<select id="kalF_color" style="background:var(--bg1);border:1px solid var(--rim);color:var(--t1);padding:8px;border-radius:4px;font-size:13px">
<option value="b" ${(!ev.color||ev.color==='b')?'selected':''}>plava</option>
<option value="g" ${ev.color==='g'?'selected':''}>zelena</option>
<option value="a" ${ev.color==='a'?'selected':''}>amber</option>
<option value="r" ${ev.color==='r'?'selected':''}>crvena</option>
</select>
</label>
</div>
</div>
<div style="padding:12px 16px;border-top:1px solid var(--rim);display:flex;justify-content:space-between;gap:8px">
<div>
${isEdit ? `<button class="btn sm" style="background:rgba(220,38,38,0.18);border-color:rgba(220,38,38,0.5);color:#fca5a5" onclick="kalDelete(${ev.id})">🗑 Obriši</button>` : ''}
</div>
<div style="display:flex;gap:8px">
<button class="btn sm" onclick="kalCloseModal()">Odustani</button>
<button class="btn primary sm" onclick="kalSave(${ev.id||'null'})">${isEdit?'Spremi':'Kreiraj'}</button>
</div>
</div>
</div>
</div>`;
}
function kalOpenModal(ev){
const wrap = document.createElement('div');
wrap.id = 'kalModalWrap';
wrap.innerHTML = kalEventModalHtml(ev);
document.body.appendChild(wrap);
setTimeout(() => { try { document.getElementById('kalF_title').focus(); } catch(e){} }, 50);
}
function kalCloseModal(){
const w = document.getElementById('kalModalWrap');
if(w) w.remove();
}
async function kalRefresh(){
const ym = window._kalState.ym || (new Date().getFullYear()+'-'+String(new Date().getMonth()+1).padStart(2,'0'));
$('#content').innerHTML = '<div class=loading>Učitavanje...</div>';
$('#content').innerHTML = await renderKalendar({ym});
}
async function kalSave(eid){
const title = (document.getElementById('kalF_title').value||'').trim();
const start = document.getElementById('kalF_start').value;
const end = document.getElementById('kalF_end').value;
const loc = document.getElementById('kalF_loc').value;
const desc = document.getElementById('kalF_desc').value;
const typ = document.getElementById('kalF_type').value;
const col = document.getElementById('kalF_color').value;
if(!title){ alert('Naziv je obavezan.'); return; }
if(!start){ alert('Početak je obavezan.'); return; }
const toIso = (s) => s ? new Date(s).toISOString() : null;
const body = {
title,
start_at: toIso(start),
end_at: end ? toIso(end) : null,
location: loc || null,
description: desc || null,
event_type: typ,
color: col,
};
let res;
if(eid && eid !== 'null'){
res = await apiAuth('/v2/kalendar/events/'+eid, {method:'PUT', body: JSON.stringify(body)});
} else {
res = await apiAuth('/v2/kalendar/events', {method:'POST', body: JSON.stringify(body)});
}
if(!res || res.__error || res.__unauthorized){
alert('Greška pri spremanju (status '+(res && res.status || '?')+').');
return;
}
kalCloseModal();
await kalRefresh();
}
async function kalEdit(eid){
const r = await apiAuth('/v2/kalendar/events/'+eid);
if(!r || r.__error){ alert('Ne mogu dohvatiti termin.'); return; }
kalOpenModal(r);
}
async function kalDelete(eid){
if(!confirm('Obrisati termin #'+eid+'?')) return;
const r = await apiAuth('/v2/kalendar/events/'+eid, {method:'DELETE'});
if(!r || r.__error){ alert('Greška pri brisanju.'); return; }
kalCloseModal();
await kalRefresh();
}
async function renderKalendar(opts){
opts = opts || {};
const today = new Date();
const ym = opts.ym || (today.getFullYear() + '-' + String(today.getMonth()+1).padStart(2,'0'));
window._kalState.ym = ym;
const [Y, M] = ym.split('-').map(Number);
const first = new Date(Y, M-1, 1);
const last = new Date(Y, M, 0);
// Učitaj liječničke + manifestacije + notifikacije + kalendar_events
let lij = [], manif = [], notif = [], kalEvents = [];
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){}
kalEvents = await kalLoadEvents(ym);
window._kalState.events = kalEvents;
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'}); });
// CRUD events from kalendar_events table
kalEvents.forEach(k => events.push({
id: k.id,
date: (k.start_at||'').substring(0,10),
type: k.event_type || 'event',
title: k.title,
klub: k.location || '',
color: k.color || 'b',
crud: true,
}));
// 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 => {
const click = e.crud && e.id ? `onclick="event.stopPropagation();kalEdit(${e.id})"` : '';
const cur = e.crud ? 'cursor:pointer;' : '';
return `<div ${click} style="${cur}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):''}${e.crud?' (klikni za uredi)':''}">${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>` : '';
// Click on empty cell → create event for that date at 09:00
const cellClick = `onclick="kalOpenModal({start_at:'${k}T09:00:00'})"`;
grid += `<div ${cellClick} 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;cursor:pointer" title="Klik za novi termin"><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 => {
const actions = e.crud && e.id
? `<button class="btn sm" onclick="kalEdit(${e.id})">Uredi</button> <button class="btn sm" style="color:#fca5a5" onclick="kalDelete(${e.id})">🗑</button>`
: '<span style="color:var(--t3);font-size:11px">—</span>';
return `<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><td>${actions}</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="kalOpenModal({})"> Novi termin</button>
<button class="btn 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 class="card-actions"><button class="btn primary sm" onclick="kalOpenModal({})"> Novi termin</button></div>
</div>
<table>
<thead><tr><th>Datum</th><th>Naziv</th><th>Lokacija/Klub</th><th>Tip</th><th>Akcije</th></tr></thead>
<tbody>${upcomingHtml || '<tr><td colspan="5" 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;
// ─── NOTIF CENTER (W5 / Agent C) ────────────────────────────────────────────
// Backend: /api/v2/notif/{list,count,{id}/read,mark-all-read,DELETE {id}}
// Renders Palantir-style cards with kind badge + relative time + actions.
const _NOTIF_KIND = {
info: {bg:'#1e3a5f', fg:'#7dd3fc', label:'INFO', ic:''},
warning: {bg:'#5a4a16', fg:'#fcd34d', label:'WARNING', ic:'⚠'},
alert: {bg:'#5a1f1f', fg:'#fca5a5', label:'ALERT', ic:'!'},
success: {bg:'#1f5a2c', fg:'#86efac', label:'OK', ic:'✓'},
};
function _notifRel(iso){
if(!iso) return '';
try {
const d = new Date(iso);
const s = Math.max(0, Math.floor((Date.now()-d.getTime())/1000));
if(s < 60) return 'upravo sada';
if(s < 3600) return Math.floor(s/60)+' min';
if(s < 86400) return Math.floor(s/3600)+' h';
if(s < 604800) return Math.floor(s/86400)+' d';
return d.toLocaleDateString('hr-HR');
} catch(e){ return ''; }
}
async function notifApi(path, opts){
opts = opts || {};
opts.headers = Object.assign({'Content-Type':'application/json'}, opts.headers||{});
const tok = (typeof getToken==='function' ? getToken() : '') || (localStorage.getItem('jwt')||localStorage.getItem('access_token')||'');
if(tok) opts.headers['Authorization'] = 'Bearer '+tok;
const r = await fetch('/sport'+path, opts);
return r.json();
}
async function renderNotifCenter(){
let data = {rows:[], count:0};
try { data = await notifApi('/api/v2/notif/list?limit=100'); } catch(e){}
const rows = data.rows || [];
const unread = rows.filter(r => !r.is_read).length;
const card = (n) => {
const k = _NOTIF_KIND[n.kind] || _NOTIF_KIND.info;
const linkBtn = n.link
? `<a class="btn sm" href="${esc(n.link)}" style="text-decoration:none">↗ Otvori</a>`
: '';
const readBtn = n.is_read
? `<span class="tag" style="opacity:0.55">Pročitano</span>`
: `<button class="btn sm primary" onclick="notifMarkRead(${n.id})">✓ Pročitano</button>`;
return `
<div class="alert-card" style="border-left:3px solid ${k.fg};display:flex;gap:12px;align-items:flex-start;padding:12px;margin-bottom:8px;background:${n.is_read?'rgba(255,255,255,0.02)':'rgba(255,255,255,0.04)'};border-radius:6px">
<div style="flex:0 0 64px;text-align:center">
<div style="background:${k.bg};color:${k.fg};font-weight:700;font-size:10px;letter-spacing:0.5px;padding:4px 6px;border-radius:3px;display:inline-block">${k.ic} ${k.label}</div>
</div>
<div style="flex:1;min-width:0">
<div style="font-weight:600;font-size:13px;color:var(--t1,#e6edf3)">${esc(n.title || n.subject || '—')}</div>
<div style="font-size:12px;color:var(--t2,#a8b3bd);margin-top:4px;white-space:pre-wrap">${esc((n.body||'').substring(0,400))}${(n.body||'').length>400?'…':''}</div>
<div style="font-size:11px;color:var(--t3,#788490);margin-top:6px">${esc(_notifRel(n.created_at))}${n.user_id?' · za korisnika #'+n.user_id:' · sustavna obavijest'}</div>
</div>
<div style="display:flex;flex-direction:column;gap:6px;align-items:flex-end">
${linkBtn}
${readBtn}
<button class="btn sm" onclick="notifDelete(${n.id})" title="Obriši">🗑</button>
</div>
</div>`;
};
return `
<div class="card">
<div class="card-h">
<div class="card-t">🔔 Notifikacijski centar <span class="tag" style="margin-left:8px">${rows.length} ukupno</span> ${unread?`<span class="tag rd" style="margin-left:4px">${unread} nepročitano</span>`:''}</div>
<div class="card-actions">
<button class="btn sm" onclick="notifReload()">↻ Osvježi</button>
<button class="btn sm primary" onclick="notifMarkAllRead()" ${unread?'':'disabled'}>✓ Označi sve kao pročitano</button>
</div>
</div>
<div style="padding:8px 0">
${rows.length ? rows.map(card).join('') : '<div class="empty" style="padding:32px;text-align:center;color:var(--t3,#788490)">Nema notifikacija.</div>'}
</div>
</div>`;
}
async function notifMarkRead(id){
try { await notifApi('/api/v2/notif/'+id+'/read', {method:'POST'}); } catch(e){}
notifReload(); notifRefreshBadge();
}
async function notifDelete(id){
if(!confirm('Obrisati notifikaciju?')) return;
try { await notifApi('/api/v2/notif/'+id, {method:'DELETE'}); } catch(e){}
notifReload(); notifRefreshBadge();
}
async function notifMarkAllRead(){
try { await notifApi('/api/v2/notif/mark-all-read', {method:'POST', body:'{}'}); } catch(e){}
notifReload(); notifRefreshBadge();
}
function notifReload(){
if(_state.section === 'notif') loadSection();
}
async function notifRefreshBadge(){
try {
const d = await notifApi('/api/v2/notif/count');
const n = (d && d.unread) || 0;
// Update in-page nav badge
document.querySelectorAll('.nav-i[data-id="notif"], #pgz-sb .pgz-nav-i[data-id="notif"]').forEach(el => {
let b = el.querySelector('.badge.notif-badge');
if(n > 0){
if(!b){
b = document.createElement('span');
b.className = 'badge notif-badge';
b.style.cssText = 'background:#dc2626;color:#fff;border-radius:10px;padding:1px 6px;font-size:10px;font-weight:700;margin-left:auto';
el.appendChild(b);
}
b.textContent = String(n);
} else if(b){
b.remove();
}
});
} catch(e){}
}
SECTIONS['pgz:notif'] = renderNotifCenter;
SECTIONS['savez:notif'] = renderNotifCenter;
SECTIONS['klub:notif'] = renderNotifCenter;
SECTIONS['sportas:notif'] = renderNotifCenter;
// Start polling badge every 30s + once on load
try {
notifRefreshBadge();
setInterval(notifRefreshBadge, 30000);
} catch(e){}
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>`;
// ─── pgz:dokumenti — knjižnica godišnjaka (in-page) ────────────────────
// Prikazuje grid kartica za 18+ godišnjaka iz pgz_sport.dokumenti.
// Klik na karticu → otvara PDF u novom tabu preko /api/v2/dokumenti/godisnjak/{godina}.
SECTIONS['pgz:dokumenti'] = async () => {
// Cache godisnjaci na _state da ne dohvaćamo svaki put
if(!_state._godisnjaci){
try {
const r = await fetch('/sport/api/v2/dokumenti/godisnjaci/list');
const j = await r.json();
_state._godisnjaci = (j && j.godisnjaci) || [];
} catch(e) { _state._godisnjaci = []; }
}
const docs = _state._godisnjaci.slice().sort((a,b)=> (b.godina||9999) - (a.godina||9999));
const fmtMB = b => b ? (b/1024/1024).toFixed(1)+' MB' : '—';
const cards = docs.map(d => {
const yr = d.godina || (d.izdano_datum ? String(d.izdano_datum).slice(0,4) : '—');
const url = d.godina ? `/sport/api/v2/dokumenti/godisnjak/${d.godina}` : `/sport/api/v2/dokumenti/${d.id}/pdf`;
return `
<div class="card" style="cursor:pointer;transition:transform 0.15s, border-color 0.15s"
onmouseover="this.style.borderColor='var(--gold,#F4C430)';this.style.transform='translateY(-2px)'"
onmouseout="this.style.borderColor='';this.style.transform=''"
onclick="window.open('${url}','_blank','noopener')">
<div style="font-size:1.9rem;font-weight:700;color:var(--gold,#F4C430);line-height:1;margin-bottom:6px;letter-spacing:-1px">${esc(yr)}</div>
<div style="font-weight:600;font-size:0.92rem;margin-bottom:6px">${esc(d.title || '(bez naslova)')}</div>
<div style="color:var(--t2);font-size:11px;margin-bottom:4px">🏛️ ${esc(d.organizacija || '—')}</div>
<div style="color:var(--t4);font-size:11px">📄 ${fmtMB(d.sadrzaj_size)}</div>
</div>`;
}).join('');
return `
<div class="card">
<div class="card-h">
<div class="card-t">📚 Dokumenti — Godišnjaci ZSP PGŽ</div>
<div class="card-actions">
<a href="/sport/dokumenti" class="btn primary" style="text-decoration:none">📖 Otvori knjižnicu</a>
</div>
</div>
<div style="color:var(--t2);font-size:12px;margin-bottom:14px">
${docs.length} godišnjaka u bazi · klik na karticu otvara PDF u novom tabu
</div>
${docs.length === 0
? '<div class="empty">Nema godišnjaka u bazi.</div>'
: `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px">${cards}</div>`}
</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>`;
// (Stari savez:kalendar mock obrisan — sada koristi renderKalendar s CRUD-om iz pgz_sport.kalendar_events)
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'];
SECTIONS['savez:putni'] = SECTIONS['pgz:putni'];
// =======================================================================
// 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="window.location.href='/erp/full?tab=uploads'">🧾 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="/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="/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'];
SECTIONS['klub:putni'] = SECTIONS['pgz:putni'];
// =======================================================================
// 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="/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="/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);
//=========== GDPR ===========
async function gdprExport() {
const tok = getToken();
if (!tok) { alert('Niste prijavljeni. Idite na /login'); return; }
try {
const r = await fetch(API + '/users/me/gdpr-export', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + tok }
});
if (!r.ok) throw new Error('HTTP ' + r.status);
const data = await r.json();
// Download as JSON file
const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'gdpr-export-' + (data.user?.email || 'me') + '-' + new Date().toISOString().slice(0,10) + '.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
alert('✓ Izvoz uspješan! Datoteka spremljena.');
} catch (e) {
alert('Greška pri izvozu: ' + e.message);
}
}
async function gdprAuditMy() {
const tok = getToken();
if (!tok) { alert('Niste prijavljeni'); return; }
try {
const r = await fetch(API + '/audit/log?user_id=me&limit=100', {
headers: { 'Authorization': 'Bearer ' + tok }
});
if (!r.ok) throw new Error('HTTP ' + r.status);
const data = await r.json();
const items = data.items || data.entries || [];
if (!items.length) {
alert('Nema audit zapisa za vaš račun.');
return;
}
const txt = items.slice(0, 30).map(e => {
const ts = new Date(e.created_at || e.timestamp).toLocaleString('hr-HR');
return ts + ' • ' + (e.action || '?') + ' • ' + (e.resource_type || '?') + ' • ' + (e.user_email || '?');
}).join('\n');
alert('Audit zapisi (zadnjih ' + Math.min(items.length, 30) + '):\n\n' + txt);
} catch (e) {
alert('Greška: ' + e.message);
}
}
async function profileDeleteAccount() {
if (!confirm('Sigurno želite zatražiti BRISANJE računa? Ovo je trajno.')) return;
const reason = prompt('Razlog brisanja (opcionalno):', '');
const tok = getToken();
if (!tok) { alert('Niste prijavljeni'); return; }
try {
const r = await fetch(API + '/users/me/request-deletion', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + tok},
body: JSON.stringify({reason: reason || ''})
});
if (r.ok) alert('✓ Zahtjev poslan. Bit ćete kontaktirani u 30 dana.');
else alert('Greška: HTTP ' + r.status);
} catch (e) {
alert('Greška: ' + e.message);
}
}
// Mobile sidebar toggle (CRISIS FIX)
function toggleMobileSidebar(){
const sb = document.getElementById('sb');
if(!sb) return;
sb.classList.toggle('mobile-open');
let backdrop = document.querySelector('.sb-backdrop');
if(!backdrop){
backdrop = document.createElement('div');
backdrop.className = 'sb-backdrop';
backdrop.onclick = () => toggleMobileSidebar();
document.body.appendChild(backdrop);
}
backdrop.classList.toggle('show');
}
// hrefnav handler — for nav items that have href (external page)
function navItemClick(item){
if(item && item.href){ window.location.href = item.href; return; }
if(item && item.id) navTo(item.id);
}
// PGŽ priority filter helpers (CRISIS V4 / SUB6)
window._pgz_filter_priority = window._pgz_filter_priority || false;
window.togglePGZFilter = function(){
window._pgz_filter_priority = !window._pgz_filter_priority;
if(typeof loadSection === 'function') loadSection();
};
window.pgzBadgePrefix = function(it){
const fin = !!(it && (it.financiran || it.klub_financiran || it.pgz_sufinanciran));
const god = !!(it && (it.godisnjak || it.klub_godisnjak || (it.godisnjak_godine && (it.godisnjak_godine.length||0)>0)));
const pgzs = !!(it && it.pgz_relevant);
const pri = !!(it && it.priority) || fin || god || pgzs;
if(!pri) return '';
let s = '⭐';
if(fin || pgzs) s += '💰';
if(god) s += '📖';
return s + ' ';
};
window.renderPGZToggleBtn = function(){
const on = !!window._pgz_filter_priority;
return '<button class="btn '+(on?'primary':'')+'" '
+ 'style="margin:6px 8px 10px 0" '
+ 'title="Prikaži samo PGŽ-financirane / u godišnjaku" '
+ 'onclick="togglePGZFilter()">'
+ (on ? '⭐ PGŽ filter ON' : '☆ PGŽ filter OFF') + '</button>';
};
</script>
<script src="/static/js/export_dropdown.js"></script>
</body>
</html>