4fc8327789
Orchestrator-side: - routers/img_proxy_router.py: 4xx/5xx → 1x1 transparent PNG (eliminates cascade <img onerror>) - static/sport2.html: removed standalone three.min.js (3d-force-graph bundles), bumped to 1.73.4 CC3 (before limit hit): - Logo home link applied to ALL HTML pages (admin.html, admin_users.html, audit.html, crm.html, erp.html, kpi.html, login.html) - Backups in _backups/*.cc3_pre_logo.$ts CC4 R3 (before plan mode): - _backups/r3_cc4/ocr.py.pre_S2.$ts Audit screenshots (80 pages) committed to _audit/audit_20260505_023639/shots/
1929 lines
114 KiB
Plaintext
1929 lines
114 KiB
Plaintext
<!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}
|
||
}
|
||
</style>
|
||
<link rel="stylesheet" href="/static/shared/sidebar.css">
|
||
<script src="/static/shared/sidebar.js" defer data-active="profil"></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">
|
||
<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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
|
||
const fmt = n => (n==null?'—':Number(n).toLocaleString('hr-HR'));
|
||
const fmtEur = n => (n==null?'—':Number(n).toLocaleString('hr-HR',{maximumFractionDigits:0})+' €');
|
||
|
||
async function api(path){
|
||
try { const r = await fetch(API+path); if(!r.ok) return null; return await r.json(); }
|
||
catch(e){ return null; }
|
||
}
|
||
|
||
// JWT-aware fetch wrapper
|
||
function getToken(){
|
||
try {
|
||
return localStorage.getItem('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(); if(tok) h['Authorization'] = 'Bearer '+tok;
|
||
if(opts.body && !(opts.body instanceof FormData) && !h['Content-Type']) h['Content-Type'] = 'application/json';
|
||
try {
|
||
const r = await fetch(API+path, Object.assign({}, opts, {headers:h}));
|
||
if(r.status === 401){ return {__unauthorized:true, status:401}; }
|
||
if(!r.ok) return {__error:true, status:r.status};
|
||
if(r.headers.get('content-type')?.includes('application/json')) return await r.json();
|
||
return {__ok:true};
|
||
} catch(e){ return {__error:true, msg:String(e)}; }
|
||
}
|
||
const initials = (n) => { if(!n) return '?'; const p=String(n).trim().split(/\s+/); return ((p[0]||'')[0]||'')+((p[1]||'')[0]||'').toUpperCase(); };
|
||
|
||
//=========== ROLES ===========
|
||
const ROLES = {
|
||
pgz: {name:'PGŽ admin', user:'Damir Radulić', av:'DR', sub:'Odjel za sport · PGŽ'},
|
||
savez: {name:'Savez admin', user:'Marija Kovač', av:'MK', sub:'Atletski savez PGŽ'},
|
||
klub: {name:'Klub admin', user:'Igor Tomić', av:'IT', sub:'AK Kvarner Rijeka'},
|
||
sportas:{name:'Sportaš', user:'Luka Horvat', av:'LH', sub:'AK Kvarner Rijeka · Trčanje 800m'},
|
||
};
|
||
|
||
const NAV_BY_ROLE = {
|
||
pgz: [
|
||
{id:'profil', ic:'\u{1F464}', label:'Moj profil'},
|
||
{id:'dashboard', ic:'\u{1F4CA}', label:'Dashboard'},
|
||
{id:'korisnici', ic:'\u{1F465}', label:'Korisnici'},
|
||
{id:'savezi', ic:'\u{1F3C5}', label:'Savezi'},
|
||
{id:'klubovi', ic:'⬢', label:'Klubovi'},
|
||
{id:'sportasi', ic:'\u{1F464}', label:'Sportaši'},
|
||
{id:'financije', ic:'€', label:'Financije'},
|
||
{id:'racuni', ic:'\u{1F9FE}', label:'Računi (OCR)'},
|
||
{id:'crm', ic:'\u{1F4DD}', label:'CRM'},
|
||
{id:'kalendar', ic:'\u{1F4C5}', label:'Kalendar'},
|
||
{id:'audit', ic:'\u{1F50D}', label:'Audit log'},
|
||
{id:'forenzika', ic:'⚠', label:'Forenzika', badge:11},
|
||
],
|
||
savez: [
|
||
{id:'profil', ic:'\u{1F464}', label:'Moj profil'},
|
||
{id:'dashboard', ic:'\u{1F4CA}', label:'Dashboard'},
|
||
{id:'klubovi', ic:'⬢', label:'Naši klubovi'},
|
||
{id:'sportasi', ic:'\u{1F464}', label:'Naši sportaši'},
|
||
{id:'zahtjevi', ic:'\u{1F4D1}', label:'Zahtjevi PGŽ', badge:3},
|
||
{id:'kalendar', ic:'\u{1F4C5}', label:'Kalendar'},
|
||
{id:'lijecnicki',ic:'⚕', label:'Liječnički'},
|
||
{id:'racuni', ic:'\u{1F9FE}', label:'Računi (OCR)'},
|
||
],
|
||
klub: [
|
||
{id:'profil', ic:'\u{1F464}', label:'Moj profil'},
|
||
{id:'dashboard', ic:'\u{1F4CA}', label:'Dashboard'},
|
||
{id:'clanovi', ic:'\u{1F465}', label:'Članovi'},
|
||
{id:'clanarine', ic:'€', label:'Članarine'},
|
||
{id:'lijecnicki',ic:'⚕', label:'Liječnički'},
|
||
{id:'dokumenti', ic:'\u{1F4C4}', label:'Dokumenti'},
|
||
{id:'kalendar', ic:'\u{1F4C5}', label:'Kalendar'},
|
||
{id:'manifestacije', ic:'\u{1F4C5}', label:'Manifestacije'},
|
||
{id:'racuni', ic:'\u{1F9FE}', label:'Računi (OCR)'},
|
||
],
|
||
sportas: [
|
||
{id:'profil', ic:'\u{1F464}', label:'Moj profil'},
|
||
{id:'dashboard', ic:'\u{1F4CA}', label:'Dashboard'},
|
||
{id:'clanarina', ic:'€', label:'Članarina'},
|
||
{id:'lijecnicki',ic:'⚕', label:'Liječnički'},
|
||
{id:'dokumenti', ic:'\u{1F4C4}', label:'Moji dokumenti'},
|
||
{id:'obrasci', ic:'\u{1F4DD}', label:'Obrasci', badge:1},
|
||
{id:'kalendar', ic:'\u{1F4C5}', label:'Kalendar'},
|
||
{id:'manifestacije', ic:'\u{1F4C5}', label:'Manifestacije'},
|
||
],
|
||
};
|
||
|
||
const _state = {role:'pgz', section:'dashboard', me:null, demoMode:true};
|
||
|
||
// Map server user_type -> UI role bucket (for nav layout)
|
||
function userTypeToRole(t){
|
||
const m = {
|
||
super_admin:'pgz', pgz_admin:'pgz', pgz_viewer:'pgz',
|
||
savez_admin:'savez',
|
||
klub_admin:'klub', klub_trener:'klub',
|
||
klub_clan:'sportas', sportas:'sportas', viewer:'pgz'
|
||
};
|
||
return m[t] || 'pgz';
|
||
}
|
||
|
||
// Try real auth first; fall back to demo mode
|
||
async function loadCurrentUser(){
|
||
if(!getToken()) return null;
|
||
const me = await apiAuth('/auth/me');
|
||
if(!me || me.__unauthorized || me.__error){
|
||
if(me && me.__unauthorized){ try { localStorage.removeItem('jwt'); } catch(e){} }
|
||
return null;
|
||
}
|
||
_state.me = me;
|
||
_state.demoMode = false;
|
||
_state.role = userTypeToRole(me.user_type);
|
||
return me;
|
||
}
|
||
function applyMeToHeader(){
|
||
const me = _state.me; if(!me) return;
|
||
const name = me.full_name || ((me.ime||'')+' '+(me.prezime||'')).trim() || me.email || '—';
|
||
const tenant = me.tenant_name || (me.tenant_type ? me.tenant_type.toUpperCase() : '');
|
||
const roleLabel = (ROLES[_state.role]||{}).name || me.user_type || 'Korisnik';
|
||
// Topbar
|
||
$('#user-name').innerHTML = esc(name) + `<span class="role-badge" id="user-role-badge">${esc(me.user_type||'')}</span>`;
|
||
$('#user-tenant').textContent = tenant;
|
||
$('#user-role-label')?.replaceChildren(document.createTextNode(roleLabel));
|
||
// Avatar topbar
|
||
if(me.avatar_url){
|
||
$('#user-av').innerHTML = `<img src="${esc(me.avatar_url)}${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||'')}</div>
|
||
<div class="kv">
|
||
<div class="k">Predsjednik</div><div class="v">${esc(d.predsjednik||'—')}</div>
|
||
<div class="k">Tajnik</div><div class="v">${esc(d.tajnik||'—')}</div>
|
||
<div class="k">Email</div><div class="v">${esc(d.email||'—')}</div>
|
||
<div class="k">Telefon</div><div class="v">${esc(d.telefon||'—')}</div>
|
||
<div class="k">Adresa</div><div class="v">${esc(d.adresa||'—')}</div>
|
||
<div class="k">Web</div><div class="v">${d.web?`<a href="${esc(d.web)}" target="_blank">${esc(d.web)}</a>`:'—'}</div>
|
||
<div class="k">Klubova</div><div class="v">${fmt(d.broj_klubova||'—')}</div>
|
||
<div class="k">Sportaša</div><div class="v">${fmt(d.broj_sportasa||'—')}</div>
|
||
<div class="k">Godina osnutka</div><div class="v">${esc(d.godina_osnutka||'—')}</div>
|
||
</div>
|
||
<div style="margin-top:14px"><a href="/sport/?savez=${id}" target="_blank" class="btn primary">Otvori u javnom portalu →</a></div>`;
|
||
}
|
||
} else if(kind === 'klub'){
|
||
const d = await api('/klubovi/'+id);
|
||
if(!d){ body = '<div class="empty">Klub nije pronađen.</div>'; }
|
||
else body = `
|
||
<h2 style="font-size:18px;color:var(--t0);margin-bottom:6px">${esc(d.naziv||'—')}</h2>
|
||
<div style="font-size:11px;color:var(--t2);margin-bottom:14px">${esc(d.savez||'')} · ${esc(d.grad||'')}</div>
|
||
<div class="kv">
|
||
<div class="k">OIB</div><div class="v">${esc(d.oib||'—')}</div>
|
||
<div class="k">Predsjednik</div><div class="v">${esc(d.predsjednik||'—')}</div>
|
||
<div class="k">Adresa</div><div class="v">${esc(d.adresa||'—')}</div>
|
||
<div class="k">Email</div><div class="v">${esc(d.email||'—')}</div>
|
||
<div class="k">Telefon</div><div class="v">${esc(d.telefon||'—')}</div>
|
||
<div class="k">Članova</div><div class="v">${fmt(d.broj_clanova||'—')}</div>
|
||
</div>`;
|
||
} else if(kind === 'zahtjev'){
|
||
const z = MOCK.zahtjevi_pending.concat(MOCK.savez_zahtjevi||[]).find(x => x.id===id || x.naziv===id) || {};
|
||
body = `
|
||
<h2 style="font-size:17px;color:var(--t0);margin-bottom:6px">${esc(z.naziv||id)}</h2>
|
||
<div class="kv">
|
||
<div class="k">Šifra</div><div class="v">${esc(z.id||'—')}</div>
|
||
<div class="k">Savez</div><div class="v">${esc(z.savez||'—')}</div>
|
||
<div class="k">Klub</div><div class="v">${esc(z.klub||'—')}</div>
|
||
<div class="k">Svrha</div><div class="v">${esc(z.svrha||'—')}</div>
|
||
<div class="k">Iznos</div><div class="v"><b style="color:var(--pgz-gold);font-size:15px">${fmtEur(z.iznos)}</b></div>
|
||
<div class="k">Datum predaje</div><div class="v">${esc(z.datum||'—')}</div>
|
||
<div class="k">Status</div><div class="v"><span class="tag am">${esc(z.status||'—')}</span></div>
|
||
</div>
|
||
<div style="margin-top:16px;display:flex;gap:8px">
|
||
<button class="btn primary">✓ Odobri</button>
|
||
<button class="btn">↩ Vrati podnositelju</button>
|
||
<button class="btn">✗ Odbij</button>
|
||
</div>
|
||
<div style="margin-top:18px;padding:14px;background:var(--bg3);border-radius:6px">
|
||
<div style="font-weight:700;color:var(--pgz-gold);font-size:11px;text-transform:uppercase;margin-bottom:8px">🔗 Blockchain seal</div>
|
||
<div style="font-size:11px;color:var(--t2)">Po odobrenju, hash zahtjeva + iznos zapisuje se u Polygon PoS (M11). Wallet: 0xD874...d368</div>
|
||
</div>`;
|
||
} else if(kind === 'audit'){
|
||
const a = MOCK.audit.concat(MOCK.audit_more||[]).find(x => x.what===id) || {ts:'',who:'',what:id};
|
||
body = `
|
||
<div class="kv">
|
||
<div class="k">Vrijeme</div><div class="v">${esc(a.ts)}</div>
|
||
<div class="k">Korisnik</div><div class="v" style="color:var(--pgz-gold)">${esc(a.who)}</div>
|
||
<div class="k">Akcija</div><div class="v">${a.what}</div>
|
||
</div>`;
|
||
} else if(kind === 'lijecnicki'){
|
||
body = `<div class="kv">
|
||
<div class="k">Sportaš</div><div class="v">${esc(id)}</div>
|
||
<div class="k">ZZJZ PGŽ</div><div class="v"><a href="https://zzjzpgz.hr" target="_blank">zzjzpgz.hr</a></div>
|
||
</div>
|
||
<div style="margin-top:14px"><button class="btn primary">📅 Zakaži termin (ZZJZ)</button></div>`;
|
||
} else if(kind === 'clan'){
|
||
body = `<h3 style="color:var(--t0);margin-bottom:10px">${esc(id)}</h3>
|
||
<div class="empty">Detalji člana — production: dohvati iz /api/clanovi/{id}</div>`;
|
||
} else {
|
||
body = '<div class="empty">Detalji.</div>';
|
||
}
|
||
} catch(e){ body = '<div class="empty">Greška pri dohvaćanju: '+esc(String(e))+'</div>'; }
|
||
$('#dpanel-body').innerHTML = body;
|
||
}
|
||
|
||
//=========== SIDEBAR ===========
|
||
function toggleSidebar(){
|
||
const sb = $('#sb');
|
||
const tg = $('#sb-toggle');
|
||
if(!sb) return;
|
||
const c = sb.classList.toggle('collapsed');
|
||
if(tg) tg.textContent = '≡';
|
||
try { localStorage.setItem('sidebar-state', c ? 'collapsed' : 'expanded'); } catch(e){}
|
||
}
|
||
function restoreSidebar(){
|
||
try {
|
||
if(localStorage.getItem('sidebar-state') === 'collapsed') $('#sb').classList.add('collapsed');
|
||
} catch(e){}
|
||
}
|
||
|
||
//=========== ROLE SWITCH ===========
|
||
function buildRoleSwitch(){
|
||
const rs = $('#role-switch');
|
||
rs.innerHTML = Object.entries(ROLES).map(([k,r]) =>
|
||
`<button data-role="${k}" onclick="setRole('${k}')" class="${k===_state.role?'active':''}">${esc(r.name)}</button>`
|
||
).join('');
|
||
}
|
||
function setRole(r){
|
||
if(!ROLES[r]) return;
|
||
_state.role = r;
|
||
_state.section = 'profil';
|
||
try { localStorage.setItem('app-role', r); } catch(e){}
|
||
$$('.role-switch button').forEach(b => b.classList.toggle('active', b.dataset.role===r));
|
||
const role = ROLES[r];
|
||
// In demo mode, populate header from ROLES table; in real-auth mode, applyMeToHeader() owns it
|
||
if(_state.demoMode){
|
||
$('#user-name').innerHTML = esc(role.user) + `<span class="role-badge">${esc(role.name)}</span>`;
|
||
$('#user-av').innerHTML = '';
|
||
$('#user-av').textContent = role.av;
|
||
$('#user-tenant').textContent = role.sub;
|
||
$('#sf-name').textContent = role.user;
|
||
$('#sf-role').textContent = role.name;
|
||
$('#sf-av').innerHTML = '';
|
||
$('#sf-av').textContent = role.av;
|
||
}
|
||
$('#role-sub').textContent = (_state.me?.tenant_name) || role.sub;
|
||
$('#role-section-label').textContent = role.name.toUpperCase();
|
||
buildNav();
|
||
navTo('profil');
|
||
}
|
||
|
||
//=========== NAV ===========
|
||
function buildNav(){
|
||
const items = NAV_BY_ROLE[_state.role] || [];
|
||
$('#nav').innerHTML = items.map(n =>
|
||
`<div class="nav-i ${n.id===_state.section?'active':''}" data-id="${n.id}" data-label="${esc(n.label)}" onclick="navTo('${n.id}')">
|
||
<span class="ic">${n.ic}</span>
|
||
<span class="lbl">${esc(n.label)}</span>
|
||
${n.badge?`<span class="badge">${n.badge}</span>`:''}
|
||
</div>`
|
||
).join('');
|
||
}
|
||
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();
|
||
}
|
||
function logout(){
|
||
if(!confirm('Odjava iz aplikacije?')) return;
|
||
try {
|
||
localStorage.removeItem('app-role');
|
||
localStorage.removeItem('jwt');
|
||
} catch(e){}
|
||
alert('Odjavljen. (Production: redirect na /login)');
|
||
window.location.href = '/static/sport2.html';
|
||
}
|
||
|
||
//=========== SECTION TITLES ===========
|
||
const TITLES = {
|
||
pgz: {
|
||
profil:['Moj profil','Osobni podaci i postavke'],
|
||
dashboard:['Dashboard','Pregled stanja PGŽ Sporta'],
|
||
korisnici:['Korisnici','Upravljanje korisnicima sustava'],
|
||
savezi:['Savezi','246 sportskih saveza'],
|
||
klubovi:['Klubovi','Sportski klubovi PGŽ'],
|
||
sportasi:['Sportaši','Registrirani članovi'],
|
||
financije:['Financije','Sufinanciranje sporta'],
|
||
racuni:['Računi (OCR)','OCR upload + obrada'],
|
||
crm:['CRM','Članarine + liječnički'],
|
||
kalendar:['Kalendar','Liječnički termini, manifestacije, eventi'],
|
||
audit:['Audit log','Sve aktivnosti sustava'],
|
||
forenzika:['Forenzika','Sumnjive transakcije / PEP'],
|
||
},
|
||
savez: {
|
||
profil:['Moj profil','Osobni podaci'],
|
||
dashboard:['Dashboard','Atletski savez PGŽ'],
|
||
klubovi:['Naši klubovi','Klubovi člana saveza'],
|
||
sportasi:['Naši sportaši','Registrirani sportaši saveza'],
|
||
zahtjevi:['Zahtjevi PGŽ','Sufinanciranje aktivnosti'],
|
||
kalendar:['Kalendar','Manifestacije saveza'],
|
||
lijecnicki:['Liječnički','Pregledi članova saveza'],
|
||
racuni:['Računi','Računi saveza'],
|
||
},
|
||
klub: {
|
||
profil:['Moj profil','Osobni podaci'],
|
||
dashboard:['Dashboard','AK Kvarner Rijeka'],
|
||
clanovi:['Članovi','Članovi kluba'],
|
||
clanarine:['Članarine','Stanje članarina'],
|
||
lijecnicki:['Liječnički','Pregledi članova'],
|
||
dokumenti:['Dokumenti','Dokumenti kluba'],
|
||
kalendar:['Kalendar','Liječnički termini + manifestacije'],
|
||
manifestacije:['Manifestacije','Nadolazeće aktivnosti'],
|
||
racuni:['Računi','Troškovi kluba'],
|
||
},
|
||
sportas: {
|
||
profil:['Moj profil','Osobni podaci'],
|
||
dashboard:['Pregled','Moja aktivnost'],
|
||
clanarina:['Članarina','Stanje moje članarine'],
|
||
lijecnicki:['Liječnički','Moj liječnički pregled'],
|
||
dokumenti:['Moji dokumenti','Suglasnosti, ugovori'],
|
||
obrasci:['Obrasci','Za potpis'],
|
||
kalendar:['Kalendar','Moji termini i događaji'],
|
||
manifestacije:['Manifestacije','Moje aktivnosti'],
|
||
},
|
||
};
|
||
|
||
function loadSection(){
|
||
const id = _state.section;
|
||
const role = _state.role;
|
||
const t = (TITLES[role] && TITLES[role][id]) || [id,''];
|
||
$('#tb-t').textContent = t[0];
|
||
$('#tb-s').textContent = t[1];
|
||
const fn = SECTIONS[role+':'+id] || SECTIONS[role+':default'] || (() => '<div class="empty">Sekcija u izradi.</div>');
|
||
$('#content').innerHTML = '<div class="loading">Učitavanje...</div>';
|
||
Promise.resolve(fn()).then(html => { $('#content').innerHTML = html || '<div class="empty">Nema podataka.</div>'; });
|
||
}
|
||
|
||
//=========== SECTION RENDERERS ===========
|
||
const SECTIONS = {};
|
||
|
||
// ──────────────────────── PROFILE PAGE (shared by all roles) ────────────────────────
|
||
function profileMe(){
|
||
// Real user if available, else demo from ROLES table
|
||
if(_state.me) return _state.me;
|
||
const r = ROLES[_state.role] || ROLES.pgz;
|
||
const parts = String(r.user||'').split(/\s+/);
|
||
return {
|
||
id: 0, email:'demo@pgz.hr',
|
||
full_name: r.user, ime: parts[0]||'', prezime: parts.slice(1).join(' '),
|
||
user_type: _state.role==='pgz'?'pgz_admin':_state.role==='savez'?'savez_admin':_state.role==='klub'?'klub_admin':'klub_clan',
|
||
tenant_type: _state.role==='pgz'?'pgz':_state.role,
|
||
tenant_name: r.sub, tenant_id: null, tier: _state.role==='pgz'?0:_state.role==='savez'?1:2,
|
||
oib: '12345678901', telefon:'+385 91 234 5678', phone:null,
|
||
last_login: '2026-05-05T00:08:09', preferred_language:'hr',
|
||
avatar_url:null, two_factor_enabled:false,
|
||
gdpr_consent_at:null, created_at:'2026-04-01T08:00:00',
|
||
roles:[{code:'demo', naziv:r.name}]
|
||
};
|
||
}
|
||
function profileRender(){
|
||
const u = profileMe();
|
||
const name = u.full_name || ((u.ime||'')+' '+(u.prezime||'')).trim() || u.email || '—';
|
||
const av = u.avatar_url ? `<img src="${esc(u.avatar_url)}" alt="">`
|
||
: (u.google_picture ? `<img src="${esc(u.google_picture)}" alt="">` : esc(initials(name)));
|
||
const lastLogin = u.last_login ? new Date(u.last_login).toLocaleString('hr-HR') : '—';
|
||
const created = u.created_at ? new Date(u.created_at).toLocaleString('hr-HR') : '—';
|
||
const gdpr = u.gdpr_consent_at ? new Date(u.gdpr_consent_at).toLocaleDateString('hr-HR') : null;
|
||
const roleLabel = (ROLES[_state.role]||{}).name || u.user_type || 'Korisnik';
|
||
return `
|
||
<div class="profile-page">
|
||
<div class="profile-banner">
|
||
<div class="profile-avatar-big" id="prof-av-big" onclick="pickAvatar()" title="Klik za upload nove slike">
|
||
${av}
|
||
<div class="upload-hint">📷 Promijeni sliku</div>
|
||
</div>
|
||
<div class="profile-banner-info">
|
||
<h1>${esc(name)}</h1>
|
||
<div class="role-line">${esc(roleLabel)} · ${esc(u.tenant_name || u.tenant_type || '')}</div>
|
||
<div class="tags-row">
|
||
<span class="tag b">${esc(u.user_type||'')}</span>
|
||
${u.aktivan!==false ? '<span class="tag gr">Aktivan</span>' : '<span class="tag rd">Suspended</span>'}
|
||
${u.two_factor_enabled ? '<span class="tag-2fa-on">2FA ON</span>' : '<span class="tag-2fa-off">2FA OFF</span>'}
|
||
${gdpr ? `<span class="tag-gdpr">GDPR ${esc(gdpr)}</span>` : ''}
|
||
</div>
|
||
</div>
|
||
<div class="profile-banner-actions">
|
||
<button class="btn" onclick="pickAvatar()">📷 Slika</button>
|
||
<button class="btn primary" onclick="profileEditAll()">✏ Uredi profil</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="profile-section">
|
||
<h3>Osobni podaci <span class="edit-link" onclick="profileEditAll()">✏ Uredi sva polja</span></h3>
|
||
<div class="profile-row" data-f="ime">
|
||
<div class="k">Ime</div>
|
||
<div class="v">${esc(u.ime||'')||'<span class="empty">—</span>'}</div>
|
||
<div class="a"><button class="btn sm" onclick="profileEditField('ime','Ime')">✏</button></div>
|
||
</div>
|
||
<div class="profile-row" data-f="prezime">
|
||
<div class="k">Prezime</div>
|
||
<div class="v">${esc(u.prezime||'')||'<span class="empty">—</span>'}</div>
|
||
<div class="a"><button class="btn sm" onclick="profileEditField('prezime','Prezime')">✏</button></div>
|
||
</div>
|
||
<div class="profile-row" data-f="full_name">
|
||
<div class="k">Puno ime</div>
|
||
<div class="v">${esc(u.full_name||'')||'<span class="empty">—</span>'}</div>
|
||
<div class="a"><button class="btn sm" onclick="profileEditField('full_name','Puno ime')">✏</button></div>
|
||
</div>
|
||
<div class="profile-row" data-f="email">
|
||
<div class="k">Email</div>
|
||
<div class="v">${esc(u.email||'—')}</div>
|
||
<div class="a"><span class="tag">read-only</span></div>
|
||
</div>
|
||
<div class="profile-row" data-f="telefon">
|
||
<div class="k">Telefon</div>
|
||
<div class="v">${esc(u.telefon||u.phone||'')||'<span class="empty">—</span>'}</div>
|
||
<div class="a"><button class="btn sm" onclick="profileEditField('telefon','Telefon')">✏</button></div>
|
||
</div>
|
||
<div class="profile-row" data-f="oib">
|
||
<div class="k">OIB</div>
|
||
<div class="v">${esc(u.oib||'')||'<span class="empty">—</span>'}</div>
|
||
<div class="a"><button class="btn sm" onclick="profileEditField('oib','OIB')">✏</button></div>
|
||
</div>
|
||
<div class="profile-row" data-f="preferred_language">
|
||
<div class="k">Jezik sučelja</div>
|
||
<div class="v">${esc(u.preferred_language||'hr')}</div>
|
||
<div class="a"><button class="btn sm" onclick="profileEditField('preferred_language','Jezik (hr/en)')">✏</button></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="profile-section">
|
||
<h3>Tenant i ovlasti</h3>
|
||
<div class="profile-row"><div class="k">Tenant</div><div class="v">${esc(u.tenant_name || '—')}</div><div class="a"></div></div>
|
||
<div class="profile-row"><div class="k">Tip tenanta</div><div class="v"><span class="tag b">${esc(u.tenant_type || '—')}</span></div><div class="a"></div></div>
|
||
<div class="profile-row"><div class="k">Tier</div><div class="v">${u.tier!=null?u.tier:'—'} ${u.tier===0?'(PGŽ)':u.tier===1?'(savez)':u.tier===2?'(klub)':''}</div><div class="a"></div></div>
|
||
<div class="profile-row"><div class="k">User type</div><div class="v"><span class="tag gd">${esc(u.user_type || '—')}</span></div><div class="a"></div></div>
|
||
<div class="profile-row"><div class="k">Dodatne uloge</div><div class="v">${(u.roles||[]).map(r => `<span class="tag b" style="margin-right:4px">${esc(r.code)}</span>`).join('')||'<span class="empty">—</span>'}</div><div class="a"></div></div>
|
||
</div>
|
||
|
||
<div class="profile-section">
|
||
<h3>Sigurnost <span class="edit-link" onclick="profileChangePassword()">🔑 Promijeni lozinku</span></h3>
|
||
<div class="profile-row"><div class="k">2FA</div><div class="v">${u.two_factor_enabled?'<span class="tag-2fa-on">Uključeno</span>':'<span class="tag-2fa-off">Nije postavljeno</span>'}</div><div class="a"><button class="btn sm primary" onclick="profileSetup2FA()">${u.two_factor_enabled?'Provjeri':'Postavi'}</button></div></div>
|
||
<div class="profile-row"><div class="k">Mora promijeniti lozinku</div><div class="v">${u.must_change_pwd?'<span class="tag rd">DA</span>':'<span class="tag gr">NE</span>'}</div><div class="a"></div></div>
|
||
<div class="profile-row"><div class="k">GDPR pristanak</div><div class="v">${gdpr || '<span class="empty">Nije zabilježen</span>'}</div><div class="a">${gdpr?'':'<button class="btn sm">Dodijeli</button>'}</div></div>
|
||
<div class="profile-row"><div class="k">Status računa</div><div class="v"><span class="tag ${u.aktivan===false?'rd':'gr'}">${u.aktivan===false?'Suspended':'Aktivan'}</span></div><div class="a"></div></div>
|
||
</div>
|
||
|
||
<div class="profile-section">
|
||
<h3>Aktivnost</h3>
|
||
<div class="profile-row"><div class="k">Zadnji login</div><div class="v" style="font-family:var(--mono);font-size:11.5px">${esc(lastLogin)}</div><div class="a"></div></div>
|
||
<div class="profile-row"><div class="k">Račun kreiran</div><div class="v" style="font-family:var(--mono);font-size:11.5px">${esc(created)}</div><div class="a"></div></div>
|
||
<div class="profile-row"><div class="k">User ID</div><div class="v" style="font-family:var(--mono)">#${esc(u.id||0)}</div><div class="a"></div></div>
|
||
</div>
|
||
|
||
<div class="profile-section">
|
||
<h3>GDPR i podaci</h3>
|
||
<div style="font-size:11.5px;color:var(--t2);line-height:1.6;margin-bottom:10px">
|
||
Imaš pravo na pristup, izmjenu i brisanje svojih osobnih podataka prema GDPR uredbi (čl. 15–17, 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 2025–2026</div></div>
|
||
<div class="chart-box"><canvas id="ch-trend"></canvas></div>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">⚡ Brze akcije</div></div>
|
||
<div style="display:grid;gap:8px">
|
||
<button class="btn primary" onclick="setRole('pgz');navTo('korisnici')">+ Dodaj korisnika</button>
|
||
<button class="btn" onclick="navTo('forenzika')">⚠ Pregled forenzike</button>
|
||
<button class="btn" onclick="navTo('racuni')">🧾 Skeniraj račun (OCR)</button>
|
||
<button class="btn" onclick="navTo('audit')">🔍 Audit log</button>
|
||
<button class="btn gold" onclick="window.open('/sport/','_blank')">🌐 Public portal</button>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">🔍 Audit log (zadnjih 6)</div></div>
|
||
${auditHtml}
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
};
|
||
|
||
// chart hook executed after innerHTML
|
||
const _origLoad = loadSection;
|
||
loadSection = function(){
|
||
_origLoad();
|
||
setTimeout(() => {
|
||
const c = document.getElementById('ch-trend');
|
||
if(c && window.Chart){
|
||
try {
|
||
new Chart(c, {
|
||
type:'line',
|
||
data:{
|
||
labels:['I','II','III','IV','V','VI','VII','VIII','IX','X','XI','XII'],
|
||
datasets:[
|
||
{label:'2025',data:[180,220,240,280,310,350,290,320,360,400,420,440],borderColor:'#8a95b4',backgroundColor:'rgba(138,149,180,.15)',tension:.35},
|
||
{label:'2026',data:[210,260,290,330,380,420,null,null,null,null,null,null],borderColor:'#F4C430',backgroundColor:'rgba(244,196,48,.18)',tension:.35,fill:true},
|
||
],
|
||
},
|
||
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{labels:{color:'#e2e6f0',font:{size:11}}}},scales:{x:{ticks:{color:'#8a95b4'},grid:{color:'#1e2a50'}},y:{ticks:{color:'#8a95b4'},grid:{color:'#1e2a50'}}}}
|
||
});
|
||
} catch(e){}
|
||
}
|
||
}, 80);
|
||
};
|
||
|
||
// PGŽ admin sub-pages
|
||
SECTIONS['pgz:korisnici'] = () => {
|
||
const rows = MOCK.korisnici.map(u => `
|
||
<tr>
|
||
<td><b>${esc(u.ime)}</b></td>
|
||
<td>${esc(u.email)}</td>
|
||
<td><span class="tag b">${esc(u.role)}</span></td>
|
||
<td>${esc(u.tenant)}</td>
|
||
<td>${esc(u.status)==='aktivan'?'<span class="tag gr">aktivan</span>':'<span class="tag rd">suspended</span>'}</td>
|
||
<td>${esc(u.last_login)}</td>
|
||
<td><button class="btn sm" onclick="alert('Audit za korisnika ${esc(u.email)}')">Audit</button></td>
|
||
</tr>`).join('');
|
||
return `
|
||
<div class="card">
|
||
<div class="card-h">
|
||
<div class="card-t">👥 Korisnici sustava (${MOCK.korisnici.length})</div>
|
||
<div class="card-actions">
|
||
<button class="btn" onclick="alert('Bulk import iz CSV')">📥 Import CSV</button>
|
||
<button class="btn primary" onclick="alert('Wizard za dodavanje korisnika')">+ Novi korisnik</button>
|
||
</div>
|
||
</div>
|
||
<table>
|
||
<thead><tr><th>Ime</th><th>Email</th><th>Uloga</th><th>Tenant</th><th>Status</th><th>Zadnji login</th><th></th></tr></thead>
|
||
<tbody>${rows}</tbody>
|
||
</table>
|
||
</div>`;
|
||
};
|
||
|
||
SECTIONS['pgz:savezi'] = async () => {
|
||
const d = await api('/savezi') || {rows:[]};
|
||
const top = (d.rows||[]).slice(0,30);
|
||
const rows = top.map(s => `
|
||
<tr style="cursor:pointer" onclick="showDetail('savez',${s.id},${JSON.stringify(s.naziv)})">
|
||
<td><b>${esc(s.naziv)}</b></td>
|
||
<td class="num">${fmt(s.broj_klubova||'—')}</td>
|
||
<td class="num">${fmt(s.broj_sportasa||'—')}</td>
|
||
<td>${esc(s.predsjednik||'—')}</td>
|
||
<td><button class="btn sm" onclick="event.stopPropagation();showDetail('savez',${s.id},${JSON.stringify(s.naziv)})">Detalji</button></td>
|
||
</tr>`).join('');
|
||
return `<div class="card"><div class="card-h"><div class="card-t">🏅 Savezi PGŽ — top 30 (od ${d.count||246})</div></div>
|
||
<table><thead><tr><th>Naziv</th><th class="num">Klubovi</th><th class="num">Sportaši</th><th>Predsjednik</th><th></th></tr></thead><tbody>${rows||'<tr><td colspan=5 class="empty">Učitavam...</td></tr>'}</tbody></table>
|
||
</div>`;
|
||
};
|
||
|
||
SECTIONS['pgz:klubovi'] = async () => {
|
||
const d = await api('/klubovi?limit=40') || {rows:[]};
|
||
const rows = (d.rows||[]).slice(0,40).map(k => `
|
||
<tr style="cursor:pointer" onclick="showDetail('klub',${k.id},${JSON.stringify(k.naziv)})">
|
||
<td><b>${esc(k.naziv)}</b></td>
|
||
<td>${esc(k.savez||'—')}</td>
|
||
<td>${esc(k.grad||'—')}</td>
|
||
<td class="num">${fmt(k.broj_clanova||'—')}</td>
|
||
<td>${esc(k.predsjednik||'—')}</td>
|
||
</tr>`).join('');
|
||
return `<div class="card"><div class="card-h"><div class="card-t">⬢ Klubovi (${d.count||0})</div></div>
|
||
<table><thead><tr><th>Naziv</th><th>Savez</th><th>Grad</th><th class="num">Članova</th><th>Predsjednik</th></tr></thead><tbody>${rows||'<tr><td colspan=5 class="empty">—</td></tr>'}</tbody></table>
|
||
</div>`;
|
||
};
|
||
|
||
SECTIONS['pgz:sportasi'] = async () => {
|
||
const d = await api('/clanovi?limit=40') || {rows:[]};
|
||
const rows = (d.rows||[]).slice(0,40).map(c => `
|
||
<tr>
|
||
<td><b>${esc(c.ime+' '+(c.prezime||''))}</b></td>
|
||
<td>${esc(c.klub||'—')}</td>
|
||
<td>${esc(c.kategorija||'—')}</td>
|
||
<td>${esc(c.spol||'—')}</td>
|
||
<td>${esc(c.datum_rodjenja||'—')}</td>
|
||
</tr>`).join('');
|
||
return `<div class="card"><div class="card-h"><div class="card-t">👤 Sportaši (${d.count||0})</div></div>
|
||
<table><thead><tr><th>Ime i prezime</th><th>Klub</th><th>Kategorija</th><th>Spol</th><th>Rođen</th></tr></thead><tbody>${rows||'<tr><td colspan=5 class="empty">—</td></tr>'}</tbody></table>
|
||
</div>`;
|
||
};
|
||
|
||
SECTIONS['pgz:financije'] = async () => {
|
||
const d = await api('/proracun') || {};
|
||
return `
|
||
<div class="kpi-grid">
|
||
<div class="kpi"><div class="kpi-l">Proračun 2026</div><div class="kpi-v">${fmtEur(d.proracun||2817309)}</div></div>
|
||
<div class="kpi g"><div class="kpi-l">Isplaćeno</div><div class="kpi-v">${fmtEur(d.isplaceno||1240000)}</div></div>
|
||
<div class="kpi a"><div class="kpi-l">U obradi</div><div class="kpi-v">${fmtEur(d.u_obradi||320000)}</div></div>
|
||
<div class="kpi r"><div class="kpi-l">Odbijeno</div><div class="kpi-v">${fmtEur(d.odbijeno||45000)}</div></div>
|
||
</div>
|
||
<div class="card"><div class="card-h"><div class="card-t">📋 Pending zahtjevi za sufinanciranje</div></div>
|
||
${MOCK.zahtjevi_pending.map(z => `
|
||
<div class="req-i">
|
||
<div class="rh"><div><div class="rt">${esc(z.naziv)}</div><div class="rsum">${esc(z.savez)} · ${esc(z.svrha)}</div></div>
|
||
<div><span class="tag am">${esc(z.status)}</span> <button class="btn sm primary">Odobri</button> <button class="btn sm">Odbij</button></div></div>
|
||
<div class="rmeta"><div>Iznos: <b>${fmtEur(z.iznos)}</b></div><div>Predano: ${esc(z.datum)}</div></div>
|
||
</div>`).join('')}
|
||
</div>`;
|
||
};
|
||
|
||
SECTIONS['pgz:racuni'] = () => `
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">🧾 OCR upload (drag & drop)</div></div>
|
||
<div style="border:2px dashed var(--rim2);border-radius:8px;padding:40px;text-align:center;background:var(--bg3);cursor:pointer" onclick="alert('OCR upload — backend M5 (CC4)')">
|
||
<div style="font-size:48px;margin-bottom:8px">📷</div>
|
||
<div style="font-weight:700;color:var(--t0);margin-bottom:4px">Dovuci ovdje sliku ili PDF računa</div>
|
||
<div style="font-size:11px;color:var(--t2)">ili klikni za odabir · cestarina, gorivo, hotel, dnevnice...</div>
|
||
<button class="btn primary" style="margin-top:12px">📸 Snimi kamerom</button>
|
||
</div>
|
||
<div style="font-size:11px;color:var(--t4);margin-top:10px">Backend: Tesseract OCR + Ri.NET AI Engine ekstrakcija polja → invoices DB</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">📋 Nedavni računi</div></div>
|
||
<table><thead><tr><th>Datum</th><th>Izdavatelj</th><th>OIB</th><th>Vrsta</th><th class="num">Iznos</th><th>Status</th></tr></thead>
|
||
<tbody>${MOCK.invoices.map(r => `<tr><td>${esc(r.datum)}</td><td><b>${esc(r.izdavatelj)}</b></td><td>${esc(r.oib)}</td><td><span class="tag ${r.tag}">${esc(r.vrsta)}</span></td><td class="num">${fmtEur(r.iznos)}</td><td>${esc(r.status)}</td></tr>`).join('')}</tbody></table>
|
||
</div>`;
|
||
|
||
SECTIONS['pgz:crm'] = () => `
|
||
<div style="margin-bottom:12px">
|
||
<a href="/crm" target="_blank" class="btn primary" style="text-decoration:none">📋 Otvori CRM workspace (Članarine • Liječnički • Obrasci) — live API</a>
|
||
</div>
|
||
<div class="row-2">
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">€ Članarine 2026</div></div>
|
||
<div class="kpi-grid" style="margin-bottom:0">
|
||
<div class="kpi g"><div class="kpi-l">Naplaćeno</div><div class="kpi-v">${fmtEur(5400)}</div></div>
|
||
<div class="kpi r"><div class="kpi-l">Dug</div><div class="kpi-v">${fmtEur(720)}</div></div>
|
||
</div>
|
||
<div style="margin-top:12px">
|
||
<button class="btn primary">📧 Notifikacija svima koji duguju</button>
|
||
<button class="btn">📄 Generiraj HUB-3 batch</button>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">⚕ Liječnički pregledi</div></div>
|
||
<div class="kpi-grid" style="margin-bottom:0">
|
||
<div class="kpi g"><div class="kpi-l">Validni</div><div class="kpi-v">${fmt(3232)}</div></div>
|
||
<div class="kpi a"><div class="kpi-l">Uskoro istek (30d)</div><div class="kpi-v">0</div></div>
|
||
<div class="kpi r"><div class="kpi-l">Istekli</div><div class="kpi-v">11</div></div>
|
||
</div>
|
||
<div style="margin-top:12px"><button class="btn primary">📅 ZZJZ PGŽ rezervacija</button></div>
|
||
</div>
|
||
</div>`;
|
||
|
||
SECTIONS['pgz:audit'] = () => `
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">🔍 Audit log — sve aktivnosti</div>
|
||
<div class="card-actions">
|
||
<input type="text" placeholder="Pretraži korisnika..." style="background:var(--bg2);border:1px solid var(--rim);border-radius:5px;padding:6px 10px;color:var(--t1);font-size:12px">
|
||
</div>
|
||
</div>
|
||
${MOCK.audit.concat(MOCK.audit_more).map(a =>
|
||
`<div class="audit-i"><div class="ts">${esc(a.ts)}</div><div class="who">${esc(a.who)}</div><div class="what">${a.what}</div></div>`
|
||
).join('')}
|
||
</div>`;
|
||
|
||
// =======================================================================
|
||
// CC5 R5 — KALENDAR (liječnički termini + manifestacije + eventi)
|
||
// =======================================================================
|
||
async function renderKalendar(opts){
|
||
opts = opts || {};
|
||
const today = new Date();
|
||
const ym = opts.ym || (today.getFullYear() + '-' + String(today.getMonth()+1).padStart(2,'0'));
|
||
const [Y, M] = ym.split('-').map(Number);
|
||
const first = new Date(Y, M-1, 1);
|
||
const last = new Date(Y, M, 0);
|
||
|
||
// Učitaj sve liječničke koji ističu unutar +180 dana, manifestacije iz API-ja, i mock eventove
|
||
let lij = [], manif = [], notif = [];
|
||
try { const d = await fetch('/sport/api/crm/lijecnicki/uskoro-isticu?days=180&include_expired=false').then(r=>r.json()); lij = d.rows || []; } catch(e){}
|
||
try { const d = await fetch('/sport/api/manifestacije').then(r=>r.json()); manif = d.rows || d || []; } catch(e){}
|
||
try { const d = await fetch('/sport/api/crm/notifications?limit=50').then(r=>r.json()); notif = d.rows || []; } catch(e){}
|
||
|
||
const events = [];
|
||
lij.forEach(l => events.push({date: l.vrijedi_do, type:'lij', title:`⚕ Pregled ističe: ${l.clan}`, klub:l.klub, color:'a'}));
|
||
manif.forEach(m => { if (m.datum) events.push({date: m.datum, type:'manif', title:`📅 ${m.naziv || m.title || 'Manifestacija'}`, klub:m.lokacija || m.grad, color:'b'}); });
|
||
// Eventi: ZZJZ termini mock — sljedećih 7 dana po radnim danima
|
||
for(let d=0; d<14; d++){
|
||
const dt = new Date(); dt.setDate(dt.getDate()+d);
|
||
if (dt.getDay()===0 || dt.getDay()===6) continue;
|
||
if ((dt.getDate() + d) % 5 === 0) {
|
||
events.push({date: dt.toISOString().slice(0,10), type:'event', title:'🏥 ZZJZ termin slot (mock)', color:'g'});
|
||
}
|
||
}
|
||
|
||
// KPI / sažetak
|
||
const cntLij = lij.length, cntManif = manif.length, cntNotif = notif.filter(n=>!n.read_at && n.channel==='inapp').length;
|
||
|
||
// Group events by date
|
||
const byDate = {};
|
||
events.forEach(e => {
|
||
if (!e.date) return;
|
||
const k = String(e.date).substring(0,10);
|
||
(byDate[k] = byDate[k] || []).push(e);
|
||
});
|
||
|
||
// Header s navigacijom
|
||
const prevYm = (() => { const d = new Date(Y, M-2, 1); return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0'); })();
|
||
const nextYm = (() => { const d = new Date(Y, M, 1); return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0'); })();
|
||
const monthName = first.toLocaleString('hr-HR', {month:'long', year:'numeric'});
|
||
|
||
// Build kalendar grid (start ponedjeljak)
|
||
let firstDow = first.getDay(); if (firstDow === 0) firstDow = 7; // pon=1, ned=7
|
||
const blanks = firstDow - 1;
|
||
const days = last.getDate();
|
||
let grid = '';
|
||
const dayNames = ['Pon','Uto','Sri','Čet','Pet','Sub','Ned'];
|
||
grid += `<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:6px">`;
|
||
dayNames.forEach(d => grid += `<div style="font-size:11px;color:var(--t3);text-align:center;font-weight:600;text-transform:uppercase;padding:4px 0">${d}</div>`);
|
||
for(let i=0; i<blanks; i++) grid += `<div></div>`;
|
||
for(let d=1; d<=days; d++){
|
||
const k = `${Y}-${String(M).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
|
||
const ev = byDate[k] || [];
|
||
const isToday = (k === today.toISOString().slice(0,10));
|
||
const evHtml = ev.slice(0,3).map(e => `<div style="font-size:10px;background:rgba(${e.color==='a'?'245,158,11':e.color==='b'?'26,115,232':'34,197,94'},0.18);padding:2px 4px;border-radius:3px;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(e.title)}${e.klub?' — '+esc(e.klub):''}">${esc(e.title.substring(0,28))}</div>`).join('');
|
||
const more = ev.length > 3 ? `<div style="font-size:9px;color:var(--t3);margin-top:2px">+${ev.length-3} više</div>` : '';
|
||
grid += `<div style="background:${isToday?'rgba(26,115,232,0.15)':'var(--bg2)'};border:1px solid ${isToday?'var(--pgz-blue)':'var(--rim)'};border-radius:6px;padding:6px;min-height:90px;${ev.length?'cursor:pointer':''}" ><div style="font-weight:600;font-size:13px;color:${isToday?'var(--pgz-blue)':'var(--t1)'}">${d}</div>${evHtml}${more}</div>`;
|
||
}
|
||
grid += '</div>';
|
||
|
||
// Lista nadolazećih (top 10)
|
||
const upcoming = events.filter(e => e.date && e.date >= today.toISOString().slice(0,10))
|
||
.sort((a,b) => a.date.localeCompare(b.date)).slice(0, 10);
|
||
const upcomingHtml = upcoming.map(e => `<tr><td>${esc(e.date)}</td><td>${esc(e.title)}</td><td>${esc(e.klub||'—')}</td><td><span class="tag ${e.color==='a'?'am':e.color==='b'?'bl':'gr'}">${e.type}</span></td></tr>`).join('');
|
||
|
||
return `
|
||
<div class="kpi-grid" style="margin-bottom:12px">
|
||
<div class="kpi a"><div class="kpi-l">⚕ Liječnički isteci</div><div class="kpi-v">${cntLij}</div><div class="kpi-s">≤ 180 dana</div></div>
|
||
<div class="kpi b"><div class="kpi-l">📅 Manifestacije</div><div class="kpi-v">${cntManif}</div></div>
|
||
<div class="kpi r"><div class="kpi-l">🔔 InApp neprocitano</div><div class="kpi-v">${cntNotif}</div></div>
|
||
<div class="kpi g"><div class="kpi-l">Eventa u kalendaru</div><div class="kpi-v">${events.length}</div></div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h">
|
||
<div class="card-t">📅 ${esc(monthName)}</div>
|
||
<div class="card-actions" style="display:flex;gap:6px;align-items:center">
|
||
<button class="btn sm" onclick="$('#content').innerHTML='<div class=loading>Učitavanje...</div>';renderKalendar({ym:'${prevYm}'}).then(h=>$('#content').innerHTML=h)">←</button>
|
||
<input type="month" value="${ym}" onchange="$('#content').innerHTML='<div class=loading>...</div>';renderKalendar({ym:this.value}).then(h=>$('#content').innerHTML=h)" style="background:var(--bg2);border:1px solid var(--rim);color:var(--t1);padding:4px 8px;border-radius:4px">
|
||
<button class="btn sm" onclick="$('#content').innerHTML='<div class=loading>Učitavanje...</div>';renderKalendar({ym:'${nextYm}'}).then(h=>$('#content').innerHTML=h)">→</button>
|
||
<button class="btn primary sm" onclick="fetch('/sport/api/crm/lijecnicki/notify-scan',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'}).then(r=>r.json()).then(d=>alert('Skenirano: '+d.created+' notifikacija kreirano'))">🔔 Scan isteke → notifikacije</button>
|
||
</div>
|
||
</div>
|
||
<div style="padding:14px">${grid}</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">📋 Nadolazeći eventi (10)</div></div>
|
||
<table>
|
||
<thead><tr><th>Datum</th><th>Naziv</th><th>Lokacija/Klub</th><th>Tip</th></tr></thead>
|
||
<tbody>${upcomingHtml || '<tr><td colspan="4" class="empty">Nema nadolazećih eventa.</td></tr>'}</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">🔔 Aktivne InApp notifikacije (10)</div>
|
||
<div class="card-actions"><button class="btn sm" onclick="fetch('/sport/api/crm/notifications/mark-all-read',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({channel:'inapp'})}).then(r=>r.json()).then(d=>{alert('Označeno '+d.marked_read+' kao pročitano');loadSection();})">Označi sve pročitano</button></div>
|
||
</div>
|
||
<div style="padding:8px 14px">
|
||
${notif.filter(n=>!n.read_at && n.channel==='inapp').slice(0,10).map(n => `
|
||
<div style="display:flex;gap:10px;align-items:start;padding:8px 0;border-bottom:1px solid var(--rim)">
|
||
<div style="font-size:18px">${n.subject.includes('ISTEKAO')?'⚠':'⚕'}</div>
|
||
<div style="flex:1">
|
||
<div style="font-weight:600;font-size:13px">${esc(n.subject)}</div>
|
||
<div style="font-size:11px;color:var(--t3);margin-top:2px">${esc((n.body||'').substring(0,140))}…</div>
|
||
</div>
|
||
<button class="btn sm" onclick="fetch('/sport/api/crm/notifications/${n.id}/read',{method:'POST'}).then(()=>loadSection())">✓</button>
|
||
</div>`).join('') || '<div class="empty">Nema neprocitanih notifikacija. Pokreni "Scan isteke" da generiraš nove.</div>'}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
SECTIONS['pgz:kalendar'] = renderKalendar;
|
||
SECTIONS['savez:kalendar'] = renderKalendar;
|
||
SECTIONS['klub:kalendar'] = renderKalendar;
|
||
SECTIONS['sportas:kalendar'] = renderKalendar;
|
||
|
||
SECTIONS['pgz:forenzika'] = () => `
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">⚠ Forenzika — sumnjive transakcije</div></div>
|
||
${MOCK.forenzika.map(f => `
|
||
<div class="alert-card ${f.sev==='crit'?'crit':''}">
|
||
<div class="at">${esc(f.title)}</div>
|
||
<div class="ad">${esc(f.desc)}</div>
|
||
<div style="margin-top:6px"><span class="tag ${f.sev==='crit'?'rd':'am'}">${esc(f.sev)}</span> <span class="tag">${esc(f.tip)}</span></div>
|
||
</div>`).join('')}
|
||
</div>`;
|
||
|
||
// =======================================================================
|
||
// SAVEZ ADMIN — Dashboard + sub-pages
|
||
// =======================================================================
|
||
SECTIONS['savez:dashboard'] = () => {
|
||
const klubHtml = MOCK.savez_klubovi.map(k => `
|
||
<div class="member-i">
|
||
<div class="av">${esc(k.naziv.substring(0,2).toUpperCase())}</div>
|
||
<div>
|
||
<div class="mn">${esc(k.naziv)}</div>
|
||
<div class="mp">${esc(k.grad)} · ${fmt(k.clanova)} članova</div>
|
||
</div>
|
||
<div class="mright">
|
||
${k.alert?'<span class="tag am">⚠</span>':'<span class="tag gr">OK</span>'}
|
||
</div>
|
||
</div>`).join('');
|
||
|
||
return `
|
||
<div class="demo-banner">
|
||
<span style="font-size:18px">🏅</span>
|
||
<div><b>Savez admin view</b> — vidiš sve klubove i sportaše u svom savezu (Atletski savez PGŽ).</div>
|
||
</div>
|
||
<div class="kpi-grid">
|
||
<div class="kpi b click" onclick="navTo('klubovi')"><div class="kpi-l">Naših klubova</div><div class="kpi-v">12</div><div class="kpi-s">aktivnih</div></div>
|
||
<div class="kpi g click" onclick="navTo('sportasi')"><div class="kpi-l">Sportaša</div><div class="kpi-v">487</div><div class="kpi-s">registriranih</div></div>
|
||
<div class="kpi a click" onclick="navTo('zahtjevi')"><div class="kpi-l">Zahtjevi PGŽ</div><div class="kpi-v">3</div><div class="kpi-s">u obradi</div></div>
|
||
<div class="kpi r click" onclick="navTo('lijecnicki')"><div class="kpi-l">Liječnički ist.</div><div class="kpi-v">7</div><div class="kpi-s">do 30 dana</div></div>
|
||
</div>
|
||
<div class="row-2">
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">⬢ Naši klubovi (12)</div><div class="card-actions"><button class="btn primary sm" onclick="navTo('klubovi')">Svi →</button></div></div>
|
||
${klubHtml}
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">📑 Naši zahtjevi PGŽ-u</div></div>
|
||
${MOCK.savez_zahtjevi.map(z => `
|
||
<div class="req-i">
|
||
<div class="rh"><div class="rt">${esc(z.naziv)}</div>
|
||
<div><span class="tag ${z.tag}">${esc(z.status)}</span></div>
|
||
</div>
|
||
<div class="rmeta"><div>Iznos: <b>${fmtEur(z.iznos)}</b></div><div>Predano: ${esc(z.datum)}</div></div>
|
||
</div>`).join('')}
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">⚕ Liječnički pregledi koji ističu (uskoro)</div></div>
|
||
<table>
|
||
<thead><tr><th>Sportaš</th><th>Klub</th><th>Datum isteka</th><th>Dana do isteka</th><th></th></tr></thead>
|
||
<tbody>${MOCK.lijecnicki_uskoro.map(l => `<tr><td><b>${esc(l.ime)}</b></td><td>${esc(l.klub)}</td><td>${esc(l.datum)}</td><td><span class="tag am">${l.dana} dana</span></td><td><button class="btn sm">Zakazat ZZJZ</button></td></tr>`).join('')}</tbody>
|
||
</table>
|
||
</div>`;
|
||
};
|
||
|
||
SECTIONS['savez:klubovi'] = () => `
|
||
<div class="card"><div class="card-h"><div class="card-t">⬢ Klubovi Atletskog saveza PGŽ (12)</div></div>
|
||
<table><thead><tr><th>Klub</th><th>Grad</th><th class="num">Članova</th><th>Predsjednik</th><th>Status</th></tr></thead>
|
||
<tbody>${MOCK.savez_klubovi_full.map(k => `<tr><td><b>${esc(k.naziv)}</b></td><td>${esc(k.grad)}</td><td class="num">${fmt(k.clanova)}</td><td>${esc(k.predsjednik)}</td><td>${k.alert?'<span class="tag am">⚠ Liječnički</span>':'<span class="tag gr">OK</span>'}</td></tr>`).join('')}</tbody></table>
|
||
</div>`;
|
||
|
||
SECTIONS['savez:sportasi'] = () => `
|
||
<div class="card"><div class="card-h"><div class="card-t">👤 Naši sportaši (487)</div></div>
|
||
<div class="kpi-grid">
|
||
<div class="kpi"><div class="kpi-l">Senior</div><div class="kpi-v">142</div></div>
|
||
<div class="kpi b"><div class="kpi-l">Juniori</div><div class="kpi-v">98</div></div>
|
||
<div class="kpi g"><div class="kpi-l">Mladi</div><div class="kpi-v">156</div></div>
|
||
<div class="kpi a"><div class="kpi-l">Reprezent.</div><div class="kpi-v">22</div></div>
|
||
</div>
|
||
<table><thead><tr><th>Ime i prezime</th><th>Klub</th><th>Disciplina</th><th>Kategorija</th><th>Liječnički</th></tr></thead>
|
||
<tbody>${MOCK.savez_sportasi.map(s => `<tr><td><b>${esc(s.ime)}</b></td><td>${esc(s.klub)}</td><td>${esc(s.disciplina)}</td><td><span class="tag b">${esc(s.kat)}</span></td><td>${s.lijecnicki==='ok'?'<span class="tag gr">OK</span>':'<span class="tag am">istek</span>'}</td></tr>`).join('')}</tbody></table>
|
||
</div>`;
|
||
|
||
SECTIONS['savez:zahtjevi'] = () => `
|
||
<div class="card"><div class="card-h"><div class="card-t">📑 Naši zahtjevi za sufinanciranje</div><div class="card-actions"><button class="btn primary">+ Novi zahtjev</button></div></div>
|
||
${MOCK.savez_zahtjevi.concat(MOCK.savez_zahtjevi_more).map(z => `
|
||
<div class="req-i">
|
||
<div class="rh"><div><div class="rt">${esc(z.naziv)}</div><div class="rsum">${esc(z.svrha||'')}</div></div>
|
||
<div><span class="tag ${z.tag}">${esc(z.status)}</span></div></div>
|
||
<div class="rmeta"><div>Iznos: <b>${fmtEur(z.iznos)}</b></div><div>Predano: ${esc(z.datum)}</div></div>
|
||
</div>`).join('')}
|
||
</div>`;
|
||
|
||
SECTIONS['savez:kalendar'] = () => `
|
||
<div class="card"><div class="card-h"><div class="card-t">📅 Kalendar manifestacija — Svibanj 2026</div></div>
|
||
<div class="cal-grid">
|
||
${'PON UTO SRI ČET PET SUB NED'.split(' ').map(h => `<div class="cal-h">${h}</div>`).join('')}
|
||
${[...Array(31)].map((_,i) => {
|
||
const day = i+1;
|
||
const ev = [4,11,18,25,9,16,30].includes(day);
|
||
const today = day===5;
|
||
return `<div class="cal-d ${today?'t':''} ${ev?'has-event':''}"><b>${day}</b>${ev?`<div style="font-size:9px;color:var(--pgz-gold);margin-top:2px">Trening / utakmica</div>`:''}</div>`;
|
||
}).join('')}
|
||
</div>
|
||
<div style="margin-top:14px;font-size:11px;color:var(--t2)">● Trening kamp Platak (4–6.5) · ● Liga PGŽ atletika (11.5) · ● Open senior (18.5) · ● Memorijalna utrka (25.5)</div>
|
||
</div>`;
|
||
|
||
SECTIONS['savez:lijecnicki'] = () => `
|
||
<div class="card"><div class="card-h"><div class="card-t">⚕ Liječnički pregledi članova saveza</div><div class="card-actions"><button class="btn primary">📅 Bulk ZZJZ rezervacija</button></div></div>
|
||
<div class="kpi-grid">
|
||
<div class="kpi g"><div class="kpi-l">Validni</div><div class="kpi-v">468</div></div>
|
||
<div class="kpi a"><div class="kpi-l">Uskoro istek</div><div class="kpi-v">7</div></div>
|
||
<div class="kpi r"><div class="kpi-l">Istekli</div><div class="kpi-v">12</div></div>
|
||
</div>
|
||
<table><thead><tr><th>Sportaš</th><th>Klub</th><th>Vrijedi do</th><th>Doktor</th><th>Status</th><th></th></tr></thead>
|
||
<tbody>${MOCK.lijecnicki_uskoro.map(l => `<tr><td><b>${esc(l.ime)}</b></td><td>${esc(l.klub)}</td><td>${esc(l.datum)}</td><td>${esc(l.doktor||'Dr. Marković')}</td><td><span class="tag am">${l.dana} dana</span></td><td><button class="btn sm">Zakaži</button></td></tr>`).join('')}</tbody></table>
|
||
</div>`;
|
||
|
||
SECTIONS['savez:racuni'] = SECTIONS['pgz:racuni'];
|
||
|
||
// =======================================================================
|
||
// KLUB ADMIN — Dashboard + sub-pages
|
||
// =======================================================================
|
||
SECTIONS['klub:dashboard'] = () => {
|
||
return `
|
||
<div class="demo-banner">
|
||
<span style="font-size:18px">⬢</span>
|
||
<div><b>Klub admin view</b> — AK Kvarner Rijeka. Upravljaš članstvom, plaćanjima i dokumentima.</div>
|
||
</div>
|
||
<div class="kpi-grid">
|
||
<div class="kpi b click" onclick="navTo('clanovi')"><div class="kpi-l">Članova</div><div class="kpi-v">87</div><div class="kpi-s">aktivnih</div><span class="kpi-trend up">+4 ovaj mjesec</span></div>
|
||
<div class="kpi g click" onclick="navTo('clanarine')"><div class="kpi-l">Naplaćeno</div><div class="kpi-v">${fmtEur(2840)}</div><div class="kpi-s">članarine 2026</div></div>
|
||
<div class="kpi r click" onclick="navTo('clanarine')"><div class="kpi-l">Dug</div><div class="kpi-v">${fmtEur(420)}</div><div class="kpi-s">7 članova</div></div>
|
||
<div class="kpi a click" onclick="navTo('lijecnicki')"><div class="kpi-l">Liječnički istek</div><div class="kpi-v">3</div><div class="kpi-s">do 30 dana</div></div>
|
||
<div class="kpi c click" onclick="navTo('lijecnicki')"><div class="kpi-l">Validni liječ.</div><div class="kpi-v">82</div><div class="kpi-s">aktivnih</div></div>
|
||
<div class="kpi click" onclick="navTo('manifestacije')"><div class="kpi-l">Manifest.</div><div class="kpi-v">5</div><div class="kpi-s">nadolazeće</div></div>
|
||
</div>
|
||
|
||
<div class="row-2">
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">👥 Najnoviji članovi</div><div class="card-actions"><button class="btn primary sm" onclick="navTo('clanovi')">Svi →</button></div></div>
|
||
${MOCK.klub_clanovi.slice(0,6).map(c => `
|
||
<div class="member-i">
|
||
<div class="av">${esc((c.ime[0]+(c.prezime?c.prezime[0]:'')).toUpperCase())}</div>
|
||
<div>
|
||
<div class="mn">${esc(c.ime+' '+c.prezime)}</div>
|
||
<div class="mp">${esc(c.kat)} · ${esc(c.discipline||'Trčanje')}</div>
|
||
</div>
|
||
<div class="mright">
|
||
${c.dug?'<span class="tag rd">Dug</span>':'<span class="tag gr">€ OK</span>'}
|
||
${c.lijecnicki==='istek'?'<span class="tag am">⚕ istek</span>':''}
|
||
</div>
|
||
</div>`).join('')}
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">⚡ Brze akcije</div></div>
|
||
<div style="display:grid;gap:8px">
|
||
<button class="btn primary" onclick="navTo('clanovi')">+ Dodaj člana</button>
|
||
<button class="btn gold" onclick="navTo('racuni')">🧾 Skeniraj račun (OCR)</button>
|
||
<button class="btn" onclick="navTo('clanarine')">€ Članarine + HUB-3</button>
|
||
<button class="btn" onclick="navTo('lijecnicki')">⚕ Liječnički bulk ZZJZ</button>
|
||
<button class="btn" onclick="alert('Obrazac sufinanciranja — M9')">📑 Predaj zahtjev PGŽ</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="row-2">
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">⚕ Pregledi koji uskoro ističu</div></div>
|
||
${MOCK.klub_lijecnicki.filter(l => l.uskoro).slice(0,4).map(l => `
|
||
<div class="alert-card">
|
||
<div class="at">${esc(l.ime)}</div>
|
||
<div class="ad">Vrijedi do: <b>${esc(l.datum)}</b> · ${esc(l.dana)} dana preostaje</div>
|
||
</div>`).join('') || '<div class="empty">Svi pregledi su važeći ✓</div>'}
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h"><div class="card-t">📅 Nadolazeće manifestacije</div></div>
|
||
${MOCK.klub_manifestacije.map(m => `
|
||
<div class="alert-card ok">
|
||
<div class="at">${esc(m.naziv)}</div>
|
||
<div class="ad">${esc(m.datum)} · ${esc(m.lokacija)} · ${esc(m.tip)}</div>
|
||
</div>`).join('')}
|
||
</div>
|
||
</div>`;
|
||
};
|
||
|
||
SECTIONS['klub:clanovi'] = () => `
|
||
<div class="card"><div class="card-h"><div class="card-t">👥 Članovi AK Kvarner Rijeka (87)</div>
|
||
<div class="card-actions"><button class="btn primary">+ Dodaj člana</button></div></div>
|
||
<table><thead><tr><th>Ime</th><th>Kategorija</th><th>Disciplina</th><th>Članarina</th><th>Liječnički</th><th>Datum upisa</th></tr></thead>
|
||
<tbody>${MOCK.klub_clanovi.map(c => `<tr><td><b>${esc(c.ime+' '+c.prezime)}</b></td><td><span class="tag b">${esc(c.kat)}</span></td><td>${esc(c.discipline||'—')}</td><td>${c.dug?'<span class="tag rd">Dug</span>':'<span class="tag gr">Plaćeno</span>'}</td><td>${c.lijecnicki==='istek'?'<span class="tag am">istek</span>':'<span class="tag gr">OK</span>'}</td><td>${esc(c.upisan||'2024-09-01')}</td></tr>`).join('')}</tbody></table>
|
||
</div>`;
|
||
|
||
SECTIONS['klub:clanarine'] = () => `
|
||
<div style="margin-bottom:10px"><a href="/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'];
|
||
|
||
// =======================================================================
|
||
// 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);
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|