CC3 R3: Sectioned sidebar redesign (DABI-style) — PORTAL/OPERATIVA/CRM/ERP/ANALITIKA/ADMIN
Reference: app.rinet.one/klasik/dabi — uppercase section headers + grouped items.
Shared module rewrite:
- /static/shared/sidebar.css v2.0
* 6 named sections, 240px expanded / 58px collapsed
* Active item: gold left-border + transparent gradient fill
* Hover: blue left-border accent
* Section header hidden in collapsed mode (replaced with dashed separator)
* Tooltip on hover (data-label) when collapsed
* Mobile <768px overlay with backdrop
- /static/shared/sidebar.js v2.0
* SIDEBAR_SECTIONS = [PORTAL, OPERATIVA, CRM, ERP, ANALITIKA, ADMIN]
* ADMIN section hidden unless user_type ∈ {pgz_admin, super_admin} (gated by /api/auth/me)
* Cross-portal links (↗ marker) for items that target a different page
* Same-page items trigger hashchange instead of full reload
* Footer = avatar + name + role + ▾ user menu (Profil / Postavke / Public portal / Prijava ↔ Odjava)
* localStorage 'sidebarCollapsed' persists across all 8 pages
Page integration:
- sport2.html ← native .sb hidden; data-active=dashboard; hashchange→navTo
- app.html ← native .sb hidden; data-active=profil; hashchange→navTo
- admin.html ← native .sidebar hidden; data-active=korisnici
- erp.html ← native .sidebar hidden; data-active=racuni
- crm.html ← data-active=clanarine
- audit.html ← data-active=audit (existing)
- kpi.html ← data-active=kpi (existing)
- login.html ← data-active=login (no item match → no highlight; user menu shows Prijava)
Backups: _backups/*.cc3_pre_redesign.{TS}
Live verified: all 8 pages HTTP 200; shared sidebar.css 200 (8664 B); sidebar.js 200 (12678 B); 6 sections present.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+12
-14
@@ -32,13 +32,9 @@ body {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
.app { display: grid; grid-template-columns: 240px 1fr; min-height: 100vh; }
|
.app { display: grid; grid-template-columns: 1fr; min-height: 100vh; }
|
||||||
.sidebar {
|
/* Native .sidebar hidden — shared sidebar (/static/shared/sidebar.*) handles sectioned menu */
|
||||||
background: var(--bg-2);
|
.sidebar { display: none; }
|
||||||
border-right: 1px solid var(--border);
|
|
||||||
padding: 20px 0;
|
|
||||||
display: flex; flex-direction: column;
|
|
||||||
}
|
|
||||||
.brand {
|
.brand {
|
||||||
padding: 0 20px 20px;
|
padding: 0 20px 20px;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
@@ -161,6 +157,8 @@ td.num { font-family: 'JetBrains Mono', monospace; text-align: right; }
|
|||||||
.sidebar { display: none; }
|
.sidebar { display: none; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<link rel="stylesheet" href="/sport/static/shared/sidebar.css">
|
||||||
|
<script src="/sport/static/shared/sidebar.js" defer data-active="korisnici"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app">
|
<div class="app">
|
||||||
@@ -206,13 +204,13 @@ td.num { font-family: 'JetBrains Mono', monospace; text-align: right; }
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="padding:14px 14px 4px 14px;font-size:9.5px;color:var(--text-dim,#8a95b4);text-transform:uppercase;letter-spacing:1.2px;font-weight:700">Portali</div>
|
<div style="padding:14px 14px 4px 14px;font-size:9.5px;color:var(--text-dim,#8a95b4);text-transform:uppercase;letter-spacing:1.2px;font-weight:700">Portali</div>
|
||||||
<a class="nav-item" href="/sport/login"><span class="icon">🔑</span><span>Prijava</span></a>
|
<a class="nav-item" href="/login"><span class="icon">🔑</span><span>Prijava</span></a>
|
||||||
<a class="nav-item" href="/sport/app"><span class="icon">📱</span><span>Aplikacija</span></a>
|
<a class="nav-item" href="/app"><span class="icon">📱</span><span>Aplikacija</span></a>
|
||||||
<a class="nav-item active" href="/sport/admin"><span class="icon">🛡</span><span>Administracija</span></a>
|
<a class="nav-item active" href="/admin"><span class="icon">🛡</span><span>Administracija</span></a>
|
||||||
<a class="nav-item" href="/sport/crm"><span class="icon">👥</span><span>CRM</span></a>
|
<a class="nav-item" href="/crm"><span class="icon">👥</span><span>CRM</span></a>
|
||||||
<a class="nav-item" href="/sport/erp"><span class="icon">💰</span><span>ERP</span></a>
|
<a class="nav-item" href="/erp"><span class="icon">💰</span><span>ERP</span></a>
|
||||||
<a class="nav-item" href="/sport/kpi"><span class="icon">📈</span><span>KPI</span></a>
|
<a class="nav-item" href="/kpi"><span class="icon">📈</span><span>KPI</span></a>
|
||||||
<a class="nav-item" href="/sport/audit"><span class="icon">📋</span><span>Audit</span></a>
|
<a class="nav-item" href="/audit"><span class="icon">📋</span><span>Audit</span></a>
|
||||||
<a class="nav-item" href="/sport/static/sport2.html" target="_blank"><span class="icon">🌐</span><span>Public portal</span></a>
|
<a class="nav-item" href="/sport/static/sport2.html" target="_blank"><span class="icon">🌐</span><span>Public portal</span></a>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|||||||
+18
-28
@@ -36,7 +36,8 @@ button,input,select,textarea{font-family:inherit;font-size:inherit;outline:none}
|
|||||||
|
|
||||||
/* ============ LAYOUT ============ */
|
/* ============ LAYOUT ============ */
|
||||||
.app{display:flex;min-height:100vh}
|
.app{display:flex;min-height:100vh}
|
||||||
.sb{width:240px;background:linear-gradient(180deg,var(--bg1) 0%,var(--bg0) 100%);border-right:1px solid var(--rim);position:fixed;top:0;left:0;bottom:0;display:flex;flex-direction:column;z-index:10;transition:width .22s ease}
|
/* 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{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{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 .logo .g{color:var(--pgz-gold)}
|
||||||
@@ -78,7 +79,7 @@ button,input,select,textarea{font-family:inherit;font-size:inherit;outline:none}
|
|||||||
.nav-ext:hover{color:var(--pgz-gold);background:var(--bg2)}
|
.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}
|
.nav-ext.active{background:linear-gradient(90deg,var(--pgz-blue) 0%,var(--pgz-blue2) 100%);color:#fff}
|
||||||
|
|
||||||
.main{margin-left:240px;flex:1;min-width:0;transition:margin-left .22s ease}
|
.main{margin-left:0;flex:1;min-width:0;transition:margin-left .22s ease}
|
||||||
.sb.collapsed ~ .main{margin-left:58px}
|
.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{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-t{font-size:15px;font-weight:700;color:var(--t0)}
|
||||||
@@ -256,6 +257,8 @@ table tbody tr:hover{background:var(--bg3)}
|
|||||||
.role-switch{display:none}
|
.role-switch{display:none}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<link rel="stylesheet" href="/sport/static/shared/sidebar.css">
|
||||||
|
<script src="/sport/static/shared/sidebar.js" defer data-active="profil"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -600,35 +603,22 @@ function setRole(r){
|
|||||||
}
|
}
|
||||||
|
|
||||||
//=========== NAV ===========
|
//=========== NAV ===========
|
||||||
const NAV_EXTERNAL = [
|
|
||||||
{id:'login', href:'/sport/login', ic:'\u{1F511}', label:'Prijava'},
|
|
||||||
{id:'app', href:'/sport/app', ic:'\u{1F4F1}', label:'Aplikacija'},
|
|
||||||
{id:'admin', href:'/sport/admin', ic:'\u{1F6E1}', label:'Administracija'},
|
|
||||||
{id:'crm', href:'/sport/crm', ic:'\u{1F465}', label:'CRM'},
|
|
||||||
{id:'erp', href:'/sport/erp', ic:'\u{1F4B0}', label:'ERP'},
|
|
||||||
{id:'kpi', href:'/sport/kpi', ic:'\u{1F4C8}', label:'KPI'},
|
|
||||||
{id:'audit', href:'/sport/audit', ic:'\u{1F4CB}', label:'Audit'},
|
|
||||||
{id:'sport2', href:'/sport/static/sport2.html', ic:'\u{1F310}', label:'Public portal'}
|
|
||||||
];
|
|
||||||
function buildNav(){
|
function buildNav(){
|
||||||
const items = NAV_BY_ROLE[_state.role] || [];
|
const items = NAV_BY_ROLE[_state.role] || [];
|
||||||
let html = items.map(n =>
|
$('#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}')">
|
`<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="ic">${n.ic}</span>
|
||||||
<span class="lbl">${esc(n.label)}</span>
|
<span class="lbl">${esc(n.label)}</span>
|
||||||
${n.badge?`<span class="badge">${n.badge}</span>`:''}
|
${n.badge?`<span class="badge">${n.badge}</span>`:''}
|
||||||
</div>`
|
</div>`
|
||||||
).join('');
|
).join('');
|
||||||
// PORTALI separator + external links (active = 'app')
|
|
||||||
html += '<div class="nav-sep" style="padding:14px 14px 4px 14px;font-size:9.5px;color:var(--t4);text-transform:uppercase;letter-spacing:1.2px;font-weight:700;white-space:nowrap;overflow:hidden">Portali</div>';
|
|
||||||
html += NAV_EXTERNAL.map(n =>
|
|
||||||
`<a class="nav-i nav-ext ${n.id==='app'?'active':''}" href="${n.href}" data-id="${n.id}" data-label="${esc(n.label)}" style="text-decoration:none">
|
|
||||||
<span class="ic">${n.ic}</span><span class="lbl">${esc(n.label)}</span>
|
|
||||||
<span style="margin-left:auto;font-size:10px;opacity:.5">↗</span>
|
|
||||||
</a>`
|
|
||||||
).join('');
|
|
||||||
$('#nav').innerHTML = html;
|
|
||||||
}
|
}
|
||||||
|
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){
|
function navTo(id){
|
||||||
_state.section = id;
|
_state.section = id;
|
||||||
$$('.nav-i').forEach(el => el.classList.toggle('active', el.dataset.id===id));
|
$$('.nav-i').forEach(el => el.classList.toggle('active', el.dataset.id===id));
|
||||||
@@ -1158,7 +1148,7 @@ SECTIONS['pgz:racuni'] = () => `
|
|||||||
|
|
||||||
SECTIONS['pgz:crm'] = () => `
|
SECTIONS['pgz:crm'] = () => `
|
||||||
<div style="margin-bottom:12px">
|
<div style="margin-bottom:12px">
|
||||||
<a href="/sport/crm" target="_blank" class="btn primary" style="text-decoration:none">📋 Otvori CRM workspace (Članarine • Liječnički • Obrasci) — live API</a>
|
<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>
|
||||||
<div class="row-2">
|
<div class="row-2">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -1255,7 +1245,7 @@ async function renderKalendar(opts){
|
|||||||
const isToday = (k === today.toISOString().slice(0,10));
|
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 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>` : '';
|
const more = ev.length > 3 ? `<div style="font-size:9px;color:var(--t3);margin-top:2px">+${ev.length-3} više</div>` : '';
|
||||||
grid += `<div style="background:${isToday?'rgba(26,115,232,0.15)':'var(--bg2)'};border:1px solid ${isToday?'var(--pgz-blue)':'var(--rim)'};border-radius:6px;padding:6px;min-height:90px;${ev.length?'cursor:pointer':''}" ${ev.length?`onclick="alert('${esc(ev.map(x=>x.title+(x.klub?' — '+x.klub:'')).join('\\n').replace(/'/g,'\\\\\\'')\)}')"`:''}><div style="font-weight:600;font-size:13px;color:${isToday?'var(--pgz-blue)':'var(--t1)'}">${d}</div>${evHtml}${more}</div>`;
|
grid += `<div 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>';
|
grid += '</div>';
|
||||||
|
|
||||||
@@ -1505,7 +1495,7 @@ SECTIONS['klub:clanovi'] = () => `
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
SECTIONS['klub:clanarine'] = () => `
|
SECTIONS['klub:clanarine'] = () => `
|
||||||
<div style="margin-bottom:10px"><a href="/sport/crm" target="_blank" class="btn primary" style="text-decoration:none">📋 Otvori live CRM (HUB-3 PDF + EPC QR generator)</a></div>
|
<div 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="row-2">
|
||||||
<div class="card"><div class="card-h"><div class="card-t">€ Članarine 2026</div></div>
|
<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>
|
<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>
|
||||||
@@ -1527,7 +1517,7 @@ SECTIONS['klub:clanarine'] = () => `
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
SECTIONS['klub:lijecnicki'] = () => `
|
SECTIONS['klub:lijecnicki'] = () => `
|
||||||
<div style="margin-bottom:10px"><a href="/sport/crm" target="_blank" class="btn primary" style="text-decoration:none">⚕ Otvori live CRM — pregledi + ZZJZ PGŽ scheduling</a></div>
|
<div 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"><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>
|
<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>
|
<table><thead><tr><th>Član</th><th>Datum pregleda</th><th>Vrijedi do</th><th>Doktor</th><th>Status</th><th></th></tr></thead>
|
||||||
@@ -1626,7 +1616,7 @@ SECTIONS['sportas:clanarina'] = () => `
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
SECTIONS['sportas:lijecnicki'] = () => `
|
SECTIONS['sportas:lijecnicki'] = () => `
|
||||||
<div style="margin-bottom:10px"><a href="/sport/crm" target="_blank" class="btn primary" style="text-decoration:none">⚕ Otvori live CRM — pregledi + ZZJZ PGŽ scheduling</a></div>`+`
|
<div 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="card"><div class="card-h"><div class="card-t">⚕ Moji liječnički pregledi</div></div>
|
||||||
<div class="alert-card">
|
<div class="alert-card">
|
||||||
<div class="at">⚠ Trenutni: vrijedi do 2026-08-15 (103 dana)</div>
|
<div class="at">⚠ Trenutni: vrijedi do 2026-08-15 (103 dana)</div>
|
||||||
@@ -1658,7 +1648,7 @@ SECTIONS['sportas:dokumenti'] = () => `
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
SECTIONS['sportas:obrasci'] = () => `
|
SECTIONS['sportas:obrasci'] = () => `
|
||||||
<div style="margin-bottom:10px"><a href="/sport/crm" target="_blank" class="btn primary" style="text-decoration:none">📝 Otvori live obrasce — popuni i digitalno potpiši</a></div>
|
<div 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="card"><div class="card-h"><div class="card-t">📝 Obrasci za potpis</div></div>
|
||||||
<div class="alert-card crit">
|
<div class="alert-card crit">
|
||||||
<div class="at">GDPR suglasnost 2026 — obvezno do 2026-06-01</div>
|
<div class="at">GDPR suglasnost 2026 — obvezno do 2026-06-01</div>
|
||||||
|
|||||||
+34
-2
@@ -136,7 +136,8 @@ table tr:hover td { background: rgba(26, 115, 232, 0.05); }
|
|||||||
.toast.err { border-left-color: var(--err); }
|
.toast.err { border-left-color: var(--err); }
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="/sport/static/shared/sidebar.css">
|
<link rel="stylesheet" href="/sport/static/shared/sidebar.css">
|
||||||
<script src="/sport/static/shared/sidebar.js" defer data-active="crm"></script>
|
<script src="/sport/static/shared/sidebar.js" defer data-active="clanarine"></script>
|
||||||
|
<style>body{padding-top:0}</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -158,6 +159,7 @@ table tr:hover td { background: rgba(26, 115, 232, 0.05); }
|
|||||||
<div class="tab" data-tab="obrasci" onclick="setTab('obrasci')">📝 Obrasci <span class="count" id="cnt-obrasci">…</span></div>
|
<div class="tab" data-tab="obrasci" onclick="setTab('obrasci')">📝 Obrasci <span class="count" id="cnt-obrasci">…</span></div>
|
||||||
<div class="tab" data-tab="stats" onclick="setTab('stats')">📊 Statistika</div>
|
<div class="tab" data-tab="stats" onclick="setTab('stats')">📊 Statistika</div>
|
||||||
<div class="tab" data-tab="notifs" onclick="setTab('notifs')">🔔 Notifikacije <span class="count" id="cnt-notifs">…</span></div>
|
<div class="tab" data-tab="notifs" onclick="setTab('notifs')">🔔 Notifikacije <span class="count" id="cnt-notifs">…</span></div>
|
||||||
|
<div class="tab" data-tab="emailtpl" onclick="setTab('emailtpl')">📨 E-mail templates</div>
|
||||||
<div style="margin-left:auto;display:flex;align-items:center;gap:8px;padding:0 14px">
|
<div style="margin-left:auto;display:flex;align-items:center;gap:8px;padding:0 14px">
|
||||||
<span style="font-size:11px;color:var(--t3)">ROLA:</span>
|
<span style="font-size:11px;color:var(--t3)">ROLA:</span>
|
||||||
<select id="g-role" onchange="setRole(this.value)" style="background:var(--bg3);border:1px solid var(--rim);color:var(--t1);padding:4px 8px;border-radius:4px;font-size:12px">
|
<select id="g-role" onchange="setRole(this.value)" style="background:var(--bg3);border:1px solid var(--rim);color:var(--t1);padding:4px 8px;border-radius:4px;font-size:12px">
|
||||||
@@ -178,6 +180,7 @@ table tr:hover td { background: rgba(26, 115, 232, 0.05); }
|
|||||||
<div id="page-obrasci" class="page" style="display:none"></div>
|
<div id="page-obrasci" class="page" style="display:none"></div>
|
||||||
<div id="page-stats" class="page" style="display:none"></div>
|
<div id="page-stats" class="page" style="display:none"></div>
|
||||||
<div id="page-notifs" class="page" style="display:none"></div>
|
<div id="page-notifs" class="page" style="display:none"></div>
|
||||||
|
<div id="page-emailtpl" class="page" style="display:none"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="modal-bg" class="modal-bg" onclick="if(event.target===this)closeModal()">
|
<div id="modal-bg" class="modal-bg" onclick="if(event.target===this)closeModal()">
|
||||||
@@ -258,6 +261,7 @@ function setTab(name) {
|
|||||||
if (name === 'obrasci') loadObrasci();
|
if (name === 'obrasci') loadObrasci();
|
||||||
if (name === 'stats') loadStats();
|
if (name === 'stats') loadStats();
|
||||||
if (name === 'notifs') loadNotifs();
|
if (name === 'notifs') loadNotifs();
|
||||||
|
if (name === 'emailtpl') loadEmailTpl();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════
|
||||||
@@ -294,7 +298,8 @@ async function loadClanarine() {
|
|||||||
<div class="grow"></div>
|
<div class="grow"></div>
|
||||||
<button class="btn" onclick="selectAllUnpaid()">☑ Sve nepladene</button>
|
<button class="btn" onclick="selectAllUnpaid()">☑ Sve nepladene</button>
|
||||||
<button class="btn primary" onclick="bulkNotifySelected()">📧 Pošalji opomenu</button>
|
<button class="btn primary" onclick="bulkNotifySelected()">📧 Pošalji opomenu</button>
|
||||||
<button class="btn" onclick="bulkUplatniceSelected()">📄 Generiraj uplatnice</button>
|
<button class="btn" onclick="bulkUplatniceSelected()">📄 Generiraj uplatnice (lista)</button>
|
||||||
|
<button class="btn" onclick="bulkUplatniceZipSelected()">🗜 Batch ZIP (PDF-ovi)</button>
|
||||||
<button class="btn" onclick="newClanarinaModal()">+ Novo zaduženje</button>
|
<button class="btn" onclick="newClanarinaModal()">+ Novo zaduženje</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="cl-bulkbar" style="display:none;background:var(--bg3);border:1px solid var(--pgz-blue);border-radius:6px;padding:8px 14px;margin-bottom:10px;align-items:center;gap:14px">
|
<div id="cl-bulkbar" style="display:none;background:var(--bg3);border:1px solid var(--pgz-blue);border-radius:6px;padding:8px 14px;margin-bottom:10px;align-items:center;gap:14px">
|
||||||
@@ -381,6 +386,33 @@ async function doBulkNotify(body) {
|
|||||||
} catch (e) { toast('Greška: ' + e.message, true); }
|
} catch (e) { toast('Greška: ' + e.message, true); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function bulkUplatniceZipSelected() {
|
||||||
|
const sel = getSelectedClanarine();
|
||||||
|
const body = sel.length ? {ids: sel.map(s => s.id), only_unpaid: false} : {};
|
||||||
|
if (!sel.length && !confirm('Ništa nije odabrano — generirati ZIP za SVE dužnike?')) return;
|
||||||
|
toast(`Generiranje ZIP-a (${sel.length || 'svi'})... može potrajati`);
|
||||||
|
try {
|
||||||
|
const r = await fetch(API + '/clanarine/bulk/uplatnice.zip', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type':'application/json'},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
if (!r.ok) {
|
||||||
|
const t = await r.text();
|
||||||
|
throw new Error(`HTTP ${r.status}: ${t.substring(0,200)}`);
|
||||||
|
}
|
||||||
|
const blob = await r.blob();
|
||||||
|
const cnt = r.headers.get('X-Batch-Count') || '?';
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `hub3-batch-${new Date().toISOString().slice(0,10)}-${cnt}.zip`;
|
||||||
|
document.body.appendChild(a); a.click(); a.remove();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
toast(`✓ ZIP preuzeto (${cnt} PDF-ova, ${(blob.size/1024).toFixed(0)} KB)`);
|
||||||
|
} catch (e) { toast('Greška: ' + e.message, true); }
|
||||||
|
}
|
||||||
|
|
||||||
async function bulkUplatniceSelected() {
|
async function bulkUplatniceSelected() {
|
||||||
const sel = getSelectedClanarine();
|
const sel = getSelectedClanarine();
|
||||||
const body = sel.length ? {ids: sel.map(s => s.id)} : {};
|
const body = sel.length ? {ids: sel.map(s => s.id)} : {};
|
||||||
|
|||||||
+12
-9
@@ -19,8 +19,9 @@
|
|||||||
}
|
}
|
||||||
* { margin:0; padding:0; box-sizing:border-box; }
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
body { font-family:'Inter',system-ui,sans-serif; background:var(--bg); color:var(--text); min-height:100vh; font-size:14px; }
|
body { font-family:'Inter',system-ui,sans-serif; background:var(--bg); color:var(--text); min-height:100vh; font-size:14px; }
|
||||||
.app { display:grid; grid-template-columns:230px 1fr; min-height:100vh; }
|
.app { display:grid; grid-template-columns:1fr; min-height:100vh; }
|
||||||
.sidebar { background:var(--bg-2); border-right:1px solid var(--border); padding:20px 0; }
|
/* Native sidebar hidden — shared sidebar (/static/shared/sidebar.*) handles sectioned menu */
|
||||||
|
.sidebar { display:none; }
|
||||||
.brand { padding:0 20px 18px; border-bottom:1px solid var(--border); margin-bottom:10px; }
|
.brand { padding:0 20px 18px; border-bottom:1px solid var(--border); margin-bottom:10px; }
|
||||||
.brand h1 { font-size:16px; font-weight:700; color:var(--accent); font-family:'JetBrains Mono',monospace; }
|
.brand h1 { font-size:16px; font-weight:700; color:var(--accent); font-family:'JetBrains Mono',monospace; }
|
||||||
.brand .sub { font-size:11px; color:var(--text-3); margin-top:2px; }
|
.brand .sub { font-size:11px; color:var(--text-3); margin-top:2px; }
|
||||||
@@ -79,6 +80,8 @@ tr.clickable:hover { background:var(--bg-3); box-shadow:inset 3px 0 0 var(--acce
|
|||||||
.actions-row { display:flex; flex-wrap:wrap; gap:8px; margin-top:14px; padding-top:14px; border-top:1px solid var(--border); }
|
.actions-row { display:flex; flex-wrap:wrap; gap:8px; margin-top:14px; padding-top:14px; border-top:1px solid var(--border); }
|
||||||
@media(max-width:768px) { .app { grid-template-columns:1fr; } .sidebar { display:none; } .grid2,.grid3 { grid-template-columns:1fr; } .col2 { grid-template-columns:1fr; } .audit-row { grid-template-columns:1fr; } }
|
@media(max-width:768px) { .app { grid-template-columns:1fr; } .sidebar { display:none; } .grid2,.grid3 { grid-template-columns:1fr; } .col2 { grid-template-columns:1fr; } .audit-row { grid-template-columns:1fr; } }
|
||||||
</style>
|
</style>
|
||||||
|
<link rel="stylesheet" href="/sport/static/shared/sidebar.css">
|
||||||
|
<script src="/sport/static/shared/sidebar.js" defer data-active="racuni"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app">
|
<div class="app">
|
||||||
@@ -91,13 +94,13 @@ tr.clickable:hover { background:var(--bg-3); box-shadow:inset 3px 0 0 var(--acce
|
|||||||
<div class="nav-item" data-tab="putni-list"><span>📋</span><span>Lista putnih naloga</span></div>
|
<div class="nav-item" data-tab="putni-list"><span>📋</span><span>Lista putnih naloga</span></div>
|
||||||
|
|
||||||
<div style="padding:14px 20px 4px;font-size:9.5px;color:var(--text-2);text-transform:uppercase;letter-spacing:1.2px;font-weight:700">Portali</div>
|
<div style="padding:14px 20px 4px;font-size:9.5px;color:var(--text-2);text-transform:uppercase;letter-spacing:1.2px;font-weight:700">Portali</div>
|
||||||
<a class="nav-item" href="/sport/login" style="text-decoration:none"><span>🔑</span><span>Prijava</span></a>
|
<a class="nav-item" href="/login" style="text-decoration:none"><span>🔑</span><span>Prijava</span></a>
|
||||||
<a class="nav-item" href="/sport/app" style="text-decoration:none"><span>📱</span><span>Aplikacija</span></a>
|
<a class="nav-item" href="/app" style="text-decoration:none"><span>📱</span><span>Aplikacija</span></a>
|
||||||
<a class="nav-item" href="/sport/admin" style="text-decoration:none"><span>🛡</span><span>Administracija</span></a>
|
<a class="nav-item" href="/admin" style="text-decoration:none"><span>🛡</span><span>Administracija</span></a>
|
||||||
<a class="nav-item" href="/sport/crm" style="text-decoration:none"><span>👥</span><span>CRM</span></a>
|
<a class="nav-item" href="/crm" style="text-decoration:none"><span>👥</span><span>CRM</span></a>
|
||||||
<a class="nav-item active" href="/sport/erp" style="text-decoration:none"><span>💰</span><span>ERP</span></a>
|
<a class="nav-item active" href="/erp" style="text-decoration:none"><span>💰</span><span>ERP</span></a>
|
||||||
<a class="nav-item" href="/sport/kpi" style="text-decoration:none"><span>📈</span><span>KPI</span></a>
|
<a class="nav-item" href="/kpi" style="text-decoration:none"><span>📈</span><span>KPI</span></a>
|
||||||
<a class="nav-item" href="/sport/audit" style="text-decoration:none"><span>📋</span><span>Audit</span></a>
|
<a class="nav-item" href="/audit" style="text-decoration:none"><span>📋</span><span>Audit</span></a>
|
||||||
<a class="nav-item" href="/sport/static/sport2.html" target="_blank" style="text-decoration:none"><span>🌐</span><span>Public portal</span></a>
|
<a class="nav-item" href="/sport/static/sport2.html" target="_blank" style="text-decoration:none"><span>🌐</span><span>Public portal</span></a>
|
||||||
</aside>
|
</aside>
|
||||||
<main class="main">
|
<main class="main">
|
||||||
|
|||||||
+91
-52
@@ -1,7 +1,7 @@
|
|||||||
/* PGŽ SPORT — Unified Sidebar v1.0
|
/* PGŽ SPORT — Unified Sectioned Sidebar v2.0
|
||||||
* dradulic@outlook.com / damir@rinet.one — 2026-05-05
|
* dradulic@outlook.com / damir@rinet.one — 2026-05-05
|
||||||
|
* Reference: app.rinet.one/klasik/dabi
|
||||||
* Used by: sport2.html, app.html, admin.html, crm.html, erp.html, audit.html, kpi.html, login.html
|
* Used by: sport2.html, app.html, admin.html, crm.html, erp.html, audit.html, kpi.html, login.html
|
||||||
* Reference: app.rinet.one/klasik/control
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
:root{
|
:root{
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
--rim:#1e2a50; --rim2:#283560;
|
--rim:#1e2a50; --rim2:#283560;
|
||||||
--t0:#fff; --t1:#e2e6f0; --t2:#8a95b4; --t4:#4e5a7a;
|
--t0:#fff; --t1:#e2e6f0; --t2:#8a95b4; --t4:#4e5a7a;
|
||||||
--green:#00e88f; --red:#ff2d55; --amber:#f59e0b; --cyan:#00c8e8;
|
--green:#00e88f; --red:#ff2d55; --amber:#f59e0b; --cyan:#00c8e8;
|
||||||
--sb-w-exp:230px; --sb-w-col:58px;
|
--sb-w-exp:240px; --sb-w-col:58px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pgz-sb{
|
#pgz-sb{
|
||||||
@@ -18,74 +18,108 @@
|
|||||||
background:linear-gradient(180deg,var(--bg1) 0%,var(--bg0) 100%);
|
background:linear-gradient(180deg,var(--bg1) 0%,var(--bg0) 100%);
|
||||||
border-right:1px solid var(--rim);
|
border-right:1px solid var(--rim);
|
||||||
display:flex; flex-direction:column; z-index:100;
|
display:flex; flex-direction:column; z-index:100;
|
||||||
font-family:'Inter',sans-serif; font-size:13px; color:var(--t1);
|
font-family:'Inter','Segoe UI',sans-serif; font-size:13px; color:var(--t1);
|
||||||
transition:width .22s ease, transform .22s ease;
|
transition:width .22s ease, transform .22s ease;
|
||||||
}
|
}
|
||||||
#pgz-sb *{box-sizing:border-box}
|
#pgz-sb *{box-sizing:border-box}
|
||||||
#pgz-sb a{text-decoration:none;color:inherit}
|
#pgz-sb a{text-decoration:none;color:inherit}
|
||||||
|
|
||||||
/* Header */
|
/* Header */
|
||||||
.pgz-sb-h{padding:18px 18px 14px;border-bottom:1px solid var(--rim);position:relative;flex-shrink:0}
|
.pgz-sb-h{padding:14px 16px 12px;border-bottom:1px solid var(--rim);position:relative;flex-shrink:0;display:flex;align-items:center;gap:10px}
|
||||||
.pgz-sb-h .pgz-logo{font-weight:800;font-size:14px;color:var(--t0);letter-spacing:.5px;white-space:nowrap;overflow:hidden}
|
.pgz-sb-h .pgz-mark{
|
||||||
|
width:28px;height:28px;border-radius:6px;flex-shrink:0;
|
||||||
|
background:linear-gradient(135deg,var(--pgz-blue) 0%,var(--pgz-blue2) 100%);
|
||||||
|
color:#fff;font-weight:800;font-size:13px;
|
||||||
|
display:flex;align-items:center;justify-content:center;
|
||||||
|
border:1px solid var(--pgz-gold);
|
||||||
|
}
|
||||||
|
.pgz-sb-h .pgz-htxt{flex:1;min-width:0;overflow:hidden}
|
||||||
|
.pgz-sb-h .pgz-logo{font-weight:800;font-size:12.5px;color:var(--t0);letter-spacing:.3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
.pgz-sb-h .pgz-logo .g{color:var(--pgz-gold)}
|
.pgz-sb-h .pgz-logo .g{color:var(--pgz-gold)}
|
||||||
.pgz-sb-h .pgz-sub{font-size:10px;color:var(--t2);margin-top:4px;text-transform:uppercase;letter-spacing:1px;white-space:nowrap;overflow:hidden}
|
.pgz-sb-h .pgz-sub{font-size:9.5px;color:var(--t2);margin-top:2px;text-transform:uppercase;letter-spacing:.6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
.pgz-sb-toggle{
|
.pgz-sb-toggle{
|
||||||
position:absolute;top:14px;right:8px;width:24px;height:24px;
|
width:24px;height:24px;flex-shrink:0;
|
||||||
display:flex;align-items:center;justify-content:center;cursor:pointer;
|
display:flex;align-items:center;justify-content:center;cursor:pointer;
|
||||||
color:var(--t2);background:var(--bg2);border:1px solid var(--rim);
|
color:var(--t2);background:var(--bg2);border:1px solid var(--rim);
|
||||||
border-radius:5px;font-size:14px;font-weight:700;
|
border-radius:5px;font-size:13px;font-weight:700;
|
||||||
transition:all .15s;user-select:none;
|
transition:all .15s;user-select:none;
|
||||||
}
|
}
|
||||||
.pgz-sb-toggle:hover{background:var(--bg3);color:var(--pgz-gold);border-color:var(--pgz-gold)}
|
.pgz-sb-toggle:hover{background:var(--bg3);color:var(--pgz-gold);border-color:var(--pgz-gold)}
|
||||||
|
|
||||||
/* Section label / separator */
|
/* Section header */
|
||||||
.pgz-sb-sep{padding:14px 14px 4px 14px;font-size:9.5px;color:var(--t4);
|
.pgz-sb-section{padding:14px 16px 4px;font-size:9.5px;color:var(--t4);
|
||||||
text-transform:uppercase;letter-spacing:1.2px;font-weight:700;
|
text-transform:uppercase;letter-spacing:1.4px;font-weight:700;
|
||||||
white-space:nowrap;overflow:hidden}
|
white-space:nowrap;overflow:hidden;
|
||||||
|
display:flex;align-items:center;gap:6px;
|
||||||
|
}
|
||||||
|
.pgz-sb-section::after{content:"";flex:1;height:1px;background:var(--rim);opacity:.7}
|
||||||
|
.pgz-sb-section.pgz-admin{color:var(--pgz-gold)}
|
||||||
|
|
||||||
/* Nav */
|
/* Nav */
|
||||||
.pgz-sb-nav{flex:1;padding:6px 8px;overflow-y:auto;overflow-x:hidden}
|
.pgz-sb-nav{flex:1;padding:4px 8px 8px;overflow-y:auto;overflow-x:hidden}
|
||||||
.pgz-sb-nav::-webkit-scrollbar{width:6px}
|
.pgz-sb-nav::-webkit-scrollbar{width:6px}
|
||||||
.pgz-sb-nav::-webkit-scrollbar-thumb{background:var(--rim2);border-radius:3px}
|
.pgz-sb-nav::-webkit-scrollbar-thumb{background:var(--rim2);border-radius:3px}
|
||||||
.pgz-nav-i{
|
.pgz-nav-i{
|
||||||
padding:9px 12px;border-radius:6px;color:var(--t2);
|
padding:7px 12px;border-radius:5px;color:var(--t2);
|
||||||
cursor:pointer;display:flex;align-items:center;gap:10px;
|
cursor:pointer;display:flex;align-items:center;gap:10px;
|
||||||
font-size:12.5px;margin-bottom:2px;white-space:nowrap;
|
font-size:12.5px;margin:1px 0;white-space:nowrap;
|
||||||
transition:background .15s,color .15s;position:relative;
|
transition:background .12s,color .12s;position:relative;
|
||||||
|
border-left:2px solid transparent;
|
||||||
}
|
}
|
||||||
.pgz-nav-i:hover{background:var(--bg2);color:var(--t1)}
|
.pgz-nav-i:hover{background:var(--bg2);color:var(--t1);border-left-color:var(--pgz-blue2)}
|
||||||
.pgz-nav-i.active{
|
.pgz-nav-i.active{
|
||||||
background:linear-gradient(90deg,var(--pgz-blue) 0%,var(--pgz-blue2) 100%);
|
background:linear-gradient(90deg,rgba(0,76,196,.18) 0%,transparent 80%);
|
||||||
color:#fff;font-weight:600;
|
color:#fff;font-weight:600;border-left-color:var(--pgz-gold);
|
||||||
}
|
}
|
||||||
.pgz-nav-i .ic{width:20px;text-align:center;font-size:14px;flex-shrink:0}
|
.pgz-nav-i.active .ic{color:var(--pgz-gold)}
|
||||||
|
.pgz-nav-i .ic{width:20px;text-align:center;font-size:13px;flex-shrink:0;color:var(--t2);transition:color .12s}
|
||||||
|
.pgz-nav-i:hover .ic{color:var(--pgz-gold)}
|
||||||
.pgz-nav-i .lbl{overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0}
|
.pgz-nav-i .lbl{overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0}
|
||||||
.pgz-nav-i .badge{margin-left:auto;background:var(--red);color:#fff;font-size:9px;font-weight:700;padding:1px 6px;border-radius:8px;flex-shrink:0}
|
.pgz-nav-i .badge{margin-left:auto;background:var(--red);color:#fff;font-size:9px;font-weight:700;padding:1px 6px;border-radius:8px;flex-shrink:0}
|
||||||
.pgz-nav-ext{color:var(--cyan)}
|
.pgz-nav-i.cross-portal::after{content:"↗";font-size:9px;opacity:.4;margin-left:6px;flex-shrink:0}
|
||||||
.pgz-nav-ext::after{content:"↗";font-size:10px;opacity:.5;margin-left:auto;flex-shrink:0}
|
.pgz-nav-i.cross-portal:hover::after{opacity:.85}
|
||||||
.pgz-nav-ext:hover{color:var(--pgz-gold);background:var(--bg2)}
|
|
||||||
.pgz-nav-ext.active{background:linear-gradient(90deg,var(--pgz-blue) 0%,var(--pgz-blue2) 100%);color:#fff}
|
|
||||||
.pgz-nav-ext.active::after{opacity:.85}
|
|
||||||
|
|
||||||
/* Footer (user) */
|
/* Footer (user) */
|
||||||
.pgz-sb-foot{padding:10px 12px;border-top:1px solid var(--rim);
|
.pgz-sb-foot{padding:8px 10px;border-top:1px solid var(--rim);
|
||||||
display:flex;align-items:center;gap:8px;
|
display:flex;align-items:center;gap:8px;
|
||||||
white-space:nowrap;overflow:hidden;flex-shrink:0}
|
white-space:nowrap;overflow:hidden;flex-shrink:0;
|
||||||
|
cursor:pointer;transition:background .15s;position:relative;
|
||||||
|
}
|
||||||
|
.pgz-sb-foot:hover{background:var(--bg2)}
|
||||||
.pgz-sb-foot .av{
|
.pgz-sb-foot .av{
|
||||||
width:30px;height:30px;border-radius:50%;
|
width:32px;height:32px;border-radius:50%;
|
||||||
background:linear-gradient(135deg,var(--pgz-blue),var(--pgz-gold));
|
background:linear-gradient(135deg,var(--pgz-blue),var(--pgz-gold));
|
||||||
color:#fff;font-weight:800;display:flex;align-items:center;justify-content:center;
|
color:#fff;font-weight:800;display:flex;align-items:center;justify-content:center;
|
||||||
font-size:11px;flex-shrink:0;overflow:hidden;
|
font-size:11px;flex-shrink:0;overflow:hidden;
|
||||||
|
border:2px solid transparent;transition:border-color .15s;
|
||||||
}
|
}
|
||||||
|
.pgz-sb-foot:hover .av{border-color:var(--pgz-gold)}
|
||||||
.pgz-sb-foot .av img{width:100%;height:100%;object-fit:cover}
|
.pgz-sb-foot .av img{width:100%;height:100%;object-fit:cover}
|
||||||
.pgz-sb-foot .ui{flex:1;min-width:0;overflow:hidden}
|
.pgz-sb-foot .ui{flex:1;min-width:0;overflow:hidden}
|
||||||
.pgz-sb-foot .un{font-size:11.5px;color:var(--t1);font-weight:600;line-height:1.2;overflow:hidden;text-overflow:ellipsis}
|
.pgz-sb-foot .un{font-size:11.5px;color:var(--t1);font-weight:600;line-height:1.2;overflow:hidden;text-overflow:ellipsis}
|
||||||
.pgz-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}
|
.pgz-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}
|
||||||
.pgz-sb-foot .lo{cursor:pointer;color:var(--t4);font-size:14px;
|
.pgz-sb-foot .caret{font-size:11px;color:var(--t4);transition:transform .15s}
|
||||||
padding:6px 8px;border-radius:5px;transition:all .15s;flex-shrink:0}
|
.pgz-sb-foot.menu-open .caret{transform:rotate(180deg)}
|
||||||
.pgz-sb-foot .lo:hover{background:rgba(255,45,85,.15);color:var(--red)}
|
|
||||||
|
|
||||||
/* Mobile burger (shown <768px when sidebar is offscreen) */
|
/* User menu (popup above footer) */
|
||||||
|
.pgz-user-menu{
|
||||||
|
position:absolute;left:8px;right:8px;bottom:54px;
|
||||||
|
background:var(--bg2);border:1px solid var(--rim);border-radius:6px;
|
||||||
|
box-shadow:0 -4px 18px rgba(0,0,0,.55);
|
||||||
|
padding:4px;z-index:300;display:none;
|
||||||
|
}
|
||||||
|
.pgz-user-menu.open{display:block}
|
||||||
|
.pgz-user-menu a{
|
||||||
|
display:flex;align-items:center;gap:8px;
|
||||||
|
padding:8px 10px;border-radius:5px;
|
||||||
|
color:var(--t1);font-size:12px;cursor:pointer;
|
||||||
|
}
|
||||||
|
.pgz-user-menu a:hover{background:var(--bg3);color:var(--pgz-gold)}
|
||||||
|
.pgz-user-menu .sep{height:1px;background:var(--rim);margin:3px 0}
|
||||||
|
.pgz-user-menu .danger{color:var(--red)}
|
||||||
|
.pgz-user-menu .danger:hover{background:rgba(255,45,85,.12)}
|
||||||
|
|
||||||
|
/* Mobile burger */
|
||||||
.pgz-sb-burger{
|
.pgz-sb-burger{
|
||||||
position:fixed;top:10px;left:10px;z-index:99;
|
position:fixed;top:10px;left:10px;z-index:99;
|
||||||
width:36px;height:36px;display:none;align-items:center;justify-content:center;
|
width:36px;height:36px;display:none;align-items:center;justify-content:center;
|
||||||
@@ -94,54 +128,59 @@
|
|||||||
}
|
}
|
||||||
.pgz-sb-burger:hover{background:var(--bg3);color:var(--pgz-gold)}
|
.pgz-sb-burger:hover{background:var(--bg3);color:var(--pgz-gold)}
|
||||||
|
|
||||||
/* Mobile X (shown <768px when sidebar is open) */
|
/* Mobile X */
|
||||||
.pgz-sb-mx{display:none;cursor:pointer;color:var(--t2);font-size:18px;
|
.pgz-sb-mx{display:none;cursor:pointer;color:var(--t2);font-size:16px;
|
||||||
width:24px;height:24px;align-items:center;justify-content:center;
|
width:24px;height:24px;align-items:center;justify-content:center;
|
||||||
border-radius:5px;transition:all .15s}
|
border-radius:5px;transition:all .15s;flex-shrink:0}
|
||||||
.pgz-sb-mx:hover{background:var(--bg3);color:var(--red)}
|
.pgz-sb-mx:hover{background:var(--bg3);color:var(--red)}
|
||||||
|
|
||||||
/* ─── Collapsed state ─── */
|
/* ─── Collapsed state ─── */
|
||||||
#pgz-sb.pgz-collapsed{width:var(--sb-w-col)}
|
#pgz-sb.pgz-collapsed{width:var(--sb-w-col)}
|
||||||
#pgz-sb.pgz-collapsed .pgz-sb-h{padding:18px 6px 14px;text-align:center}
|
#pgz-sb.pgz-collapsed .pgz-sb-h{padding:14px 6px 12px;justify-content:center;flex-direction:column;gap:8px}
|
||||||
#pgz-sb.pgz-collapsed .pgz-sb-h .pgz-logo{font-size:0}
|
#pgz-sb.pgz-collapsed .pgz-sb-h .pgz-htxt{display:none}
|
||||||
#pgz-sb.pgz-collapsed .pgz-sb-h .pgz-logo::before{content:"PG";font-size:13px;color:var(--pgz-gold);font-weight:800}
|
#pgz-sb.pgz-collapsed .pgz-sb-section{
|
||||||
#pgz-sb.pgz-collapsed .pgz-sb-h .pgz-sub{display:none}
|
font-size:0;padding:6px 0;text-align:center;
|
||||||
#pgz-sb.pgz-collapsed .pgz-sb-toggle{position:static;margin:6px auto 0;display:flex}
|
border-top:1px dashed var(--rim);margin:6px 8px 4px;
|
||||||
#pgz-sb.pgz-collapsed .pgz-sb-sep{font-size:0;padding:6px 0;text-align:center;border-top:1px dashed var(--rim);margin:6px 8px 4px}
|
}
|
||||||
#pgz-sb.pgz-collapsed .pgz-nav-i{justify-content:center;padding:10px 6px}
|
#pgz-sb.pgz-collapsed .pgz-sb-section::after{display:none}
|
||||||
|
#pgz-sb.pgz-collapsed .pgz-nav-i{justify-content:center;padding:9px 6px;border-left:none}
|
||||||
|
#pgz-sb.pgz-collapsed .pgz-nav-i.active{
|
||||||
|
background:linear-gradient(135deg,var(--pgz-blue),var(--pgz-blue2));
|
||||||
|
border-left:none;
|
||||||
|
}
|
||||||
#pgz-sb.pgz-collapsed .pgz-nav-i .lbl,
|
#pgz-sb.pgz-collapsed .pgz-nav-i .lbl,
|
||||||
#pgz-sb.pgz-collapsed .pgz-nav-i .badge,
|
#pgz-sb.pgz-collapsed .pgz-nav-i .badge,
|
||||||
#pgz-sb.pgz-collapsed .pgz-nav-ext::after{display:none}
|
#pgz-sb.pgz-collapsed .pgz-nav-i.cross-portal::after{display:none}
|
||||||
#pgz-sb.pgz-collapsed .pgz-sb-foot{padding:10px 6px;justify-content:center}
|
#pgz-sb.pgz-collapsed .pgz-sb-foot{padding:8px 6px;justify-content:center}
|
||||||
#pgz-sb.pgz-collapsed .pgz-sb-foot .ui,
|
#pgz-sb.pgz-collapsed .pgz-sb-foot .ui,
|
||||||
#pgz-sb.pgz-collapsed .pgz-sb-foot .lo{display:none}
|
#pgz-sb.pgz-collapsed .pgz-sb-foot .caret{display:none}
|
||||||
|
#pgz-sb.pgz-collapsed .pgz-user-menu{left:62px;right:auto;width:200px;bottom:8px}
|
||||||
|
|
||||||
/* Tooltip when collapsed */
|
/* Tooltip when collapsed */
|
||||||
#pgz-sb.pgz-collapsed .pgz-nav-i:hover::after{
|
#pgz-sb.pgz-collapsed .pgz-nav-i:hover::before{
|
||||||
content:attr(data-label);
|
content:attr(data-label);
|
||||||
position:absolute;left:calc(var(--sb-w-col) - 4px);top:50%;transform:translateY(-50%);
|
position:absolute;left:calc(var(--sb-w-col) - 4px);top:50%;transform:translateY(-50%);
|
||||||
background:var(--bg3);color:var(--t0);
|
background:var(--bg3);color:var(--t0);
|
||||||
padding:5px 10px;border-radius:4px;
|
padding:5px 10px;border-radius:4px;
|
||||||
font-size:11.5px;white-space:nowrap;
|
font-size:11.5px;white-space:nowrap;
|
||||||
border:1px solid var(--rim);font-weight:600;
|
border:1px solid var(--rim);font-weight:600;
|
||||||
box-shadow:2px 2px 10px rgba(0,0,0,.45);
|
box-shadow:2px 2px 10px rgba(0,0,0,.55);
|
||||||
pointer-events:none;z-index:200;
|
pointer-events:none;z-index:200;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Layout helper — apply on body to push content right of sidebar */
|
/* Layout helper: pages opt in by adding body.pgz-has-sb */
|
||||||
body.pgz-has-sb{padding-left:var(--sb-w-exp);transition:padding-left .22s ease}
|
body.pgz-has-sb{padding-left:var(--sb-w-exp);transition:padding-left .22s ease}
|
||||||
body.pgz-has-sb.pgz-sb-col{padding-left:var(--sb-w-col)}
|
body.pgz-has-sb.pgz-sb-col{padding-left:var(--sb-w-col)}
|
||||||
|
|
||||||
/* Mobile: <768px */
|
/* Mobile <768px */
|
||||||
@media (max-width:768px){
|
@media (max-width:768px){
|
||||||
#pgz-sb{transform:translateX(-100%)}
|
#pgz-sb{transform:translateX(-100%)}
|
||||||
#pgz-sb.pgz-mobile-open{transform:translateX(0)}
|
#pgz-sb.pgz-mobile-open{transform:translateX(0)}
|
||||||
#pgz-sb.pgz-collapsed{width:var(--sb-w-exp)} /* full width on mobile when open */
|
#pgz-sb.pgz-collapsed{width:var(--sb-w-exp)}
|
||||||
body.pgz-has-sb,body.pgz-has-sb.pgz-sb-col{padding-left:0}
|
body.pgz-has-sb,body.pgz-has-sb.pgz-sb-col{padding-left:0}
|
||||||
.pgz-sb-burger{display:flex}
|
.pgz-sb-burger{display:flex}
|
||||||
.pgz-sb-mx{display:flex}
|
.pgz-sb-mx{display:flex}
|
||||||
.pgz-sb-toggle{display:none}
|
.pgz-sb-toggle{display:none}
|
||||||
/* overlay backdrop */
|
|
||||||
body.pgz-mobile-sb-open::before{
|
body.pgz-mobile-sb-open::before{
|
||||||
content:"";position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:99;backdrop-filter:blur(2px)
|
content:"";position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:99;backdrop-filter:blur(2px)
|
||||||
}
|
}
|
||||||
|
|||||||
+173
-89
@@ -1,36 +1,68 @@
|
|||||||
/* PGŽ SPORT — Unified Sidebar v1.0
|
/* PGŽ SPORT — Unified Sectioned Sidebar v2.0
|
||||||
* dradulic@outlook.com / damir@rinet.one — 2026-05-05
|
* dradulic@outlook.com / damir@rinet.one — 2026-05-05
|
||||||
|
* Reference: app.rinet.one/klasik/dabi
|
||||||
*
|
*
|
||||||
* Usage on each page:
|
* Usage:
|
||||||
* <link rel="stylesheet" href="/static/shared/sidebar.css">
|
* <link rel="stylesheet" href="/sport/static/shared/sidebar.css">
|
||||||
* <script src="/static/shared/sidebar.js" defer
|
* <script src="/sport/static/shared/sidebar.js" defer
|
||||||
* data-active="app" // page key for highlight: app|admin|crm|erp|kpi|audit|login|sport2
|
* data-active="dashboard" // active item id
|
||||||
* data-inline="0"></script> // 0 (default) = render on load. 1 = call PGZSidebar.mount() yourself
|
* data-portal="portal"></script> // active portal hint (optional)
|
||||||
*
|
*
|
||||||
* The script renders #pgz-sb at start of <body>, adds class "pgz-has-sb" to body
|
* Auto-mounts <aside id="pgz-sb"> at start of <body> and adds class "pgz-has-sb" to <body>
|
||||||
* (so existing layouts can be migrated). Pages that already have their own sidebar
|
* for layout. ADMIN section is gated by user role from /api/auth/me.
|
||||||
* should pass data-skip="1" — only NAV_EXTERNAL portal links will be appended to
|
|
||||||
* an element with id="pgz-portal-mount" if present.
|
|
||||||
*/
|
*/
|
||||||
(function(){
|
(function(){
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// ────────── Configuration ──────────
|
// Sectioned menu (DABI-style).
|
||||||
// Per-portal "internal" sections (left as a hint; pages typically own their own internal nav)
|
// href can be:
|
||||||
// External portal links — same on every page
|
// "/sport/<page>" → cross-portal navigation (full page load)
|
||||||
const NAV_EXTERNAL = [
|
// "/sport/<page>#<hash>" → cross-portal + intent on that page
|
||||||
{id:'login', href:'/sport/login', ic:'\u{1F511}', label:'Prijava'},
|
// "#<id>" → in-page anchor (handled by host page on hashchange)
|
||||||
{id:'app', href:'/sport/app', ic:'\u{1F4F1}', label:'Aplikacija'},
|
const SIDEBAR_SECTIONS = [
|
||||||
{id:'admin', href:'/sport/admin', ic:'\u{1F6E1}', label:'Administracija'},
|
{title:'PORTAL', items: [
|
||||||
{id:'crm', href:'/sport/crm', ic:'\u{1F465}', label:'CRM'},
|
{id:'dashboard', ic:'\u{1F4CA}', label:'Dashboard', href:'/sport/static/sport2.html#dashboard'},
|
||||||
{id:'erp', href:'/sport/erp', ic:'\u{1F4B0}', label:'ERP'},
|
{id:'savezi', ic:'\u{1F3C5}', label:'Savezi', href:'/sport/static/sport2.html#savezi'},
|
||||||
{id:'kpi', href:'/sport/kpi', ic:'\u{1F4C8}', label:'KPI'},
|
{id:'klubovi', ic:'⬢', label:'Klubovi', href:'/sport/static/sport2.html#klubovi'},
|
||||||
{id:'audit', href:'/sport/audit', ic:'\u{1F4CB}', label:'Audit'},
|
{id:'sportasi', ic:'\u{1F464}', label:'Sportaši', href:'/sport/static/sport2.html#sportasi'},
|
||||||
{id:'sport2', href:'/sport/static/sport2.html', ic:'\u{1F310}', label:'Public portal'}
|
{id:'manifestacije', ic:'\u{1F4C5}', label:'Manifestacije', href:'/sport/static/sport2.html#manifestacije'}
|
||||||
|
]},
|
||||||
|
{title:'OPERATIVA', items: [
|
||||||
|
{id:'profil', ic:'\u{1F464}', label:'Moj profil', href:'/sport/app#profil'},
|
||||||
|
{id:'app', ic:'\u{1F4F1}', label:'Aplikacija', href:'/sport/app'},
|
||||||
|
{id:'kalendar', ic:'\u{1F4C5}', label:'Kalendar', href:'/sport/app#kalendar'},
|
||||||
|
{id:'notif', ic:'\u{1F514}', label:'Notifikacije', href:'/sport/app#notif'}
|
||||||
|
]},
|
||||||
|
{title:'CRM', items: [
|
||||||
|
{id:'clanarine', ic:'\u{1F4B3}', label:'Članarine', href:'/sport/crm#clanarine'},
|
||||||
|
{id:'lijecnicki',ic:'⚕', label:'Liječnički', href:'/sport/crm#lijecnicki'},
|
||||||
|
{id:'obrasci', ic:'\u{1F4CB}', label:'Obrasci', href:'/sport/crm#obrasci'},
|
||||||
|
{id:'dokumenti', ic:'\u{1F4C4}', label:'Dokumenti', href:'/sport/crm#dokumenti'}
|
||||||
|
]},
|
||||||
|
{title:'ERP', items: [
|
||||||
|
{id:'racuni', ic:'\u{1F9FE}', label:'Računi (OCR)', href:'/sport/erp#racuni'},
|
||||||
|
{id:'putni', ic:'✈', label:'Putni nalozi', href:'/sport/erp#putni'},
|
||||||
|
{id:'placanja', ic:'\u{1F4B0}', label:'Plaćanja', href:'/sport/erp#placanja'},
|
||||||
|
{id:'xlsx', ic:'\u{1F4C8}', label:'XLSX export', href:'/sport/erp#xlsx'}
|
||||||
|
]},
|
||||||
|
{title:'ANALITIKA', items: [
|
||||||
|
{id:'kpi', ic:'\u{1F4C8}', label:'KPI Dashboard', href:'/sport/kpi'},
|
||||||
|
{id:'financije', ic:'€', label:'Financije', href:'/sport/static/sport2.html#financije'},
|
||||||
|
{id:'mreza', ic:'\u{1F578}', label:'Mreža (graf)', href:'/sport/static/sport2.html#mreza'},
|
||||||
|
{id:'forenzika', ic:'⚠', label:'Forenzika', href:'/sport/static/sport2.html#forenzika'},
|
||||||
|
{id:'audit', ic:'\u{1F512}', label:'Audit log', href:'/sport/audit'}
|
||||||
|
]},
|
||||||
|
{title:'ADMIN', requireRole:['pgz_admin','super_admin'], items: [
|
||||||
|
{id:'korisnici', ic:'\u{1F465}', label:'Korisnici', href:'/sport/admin#korisnici'},
|
||||||
|
{id:'tenanti', ic:'\u{1F3E2}', label:'Tenanti', href:'/sport/admin#tenanti'},
|
||||||
|
{id:'sigurnost', ic:'\u{1F6E1}', label:'Sigurnost', href:'/sport/admin#sigurnost'},
|
||||||
|
{id:'sustav', ic:'⚙', label:'Sustav', href:'/sport/admin#sustav'}
|
||||||
|
]}
|
||||||
];
|
];
|
||||||
|
|
||||||
const STATE_KEY = 'sidebarCollapsed'; // shared across all pages
|
const STATE_KEY = 'sidebarCollapsed';
|
||||||
const $ = (s, root) => (root||document).querySelector(s);
|
const $ = (s, root) => (root||document).querySelector(s);
|
||||||
|
const esc = s => String(s==null?'':s).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
|
||||||
|
|
||||||
function readToken(){
|
function readToken(){
|
||||||
try { return localStorage.getItem('jwt') || localStorage.getItem('access_token') || ''; }
|
try { return localStorage.getItem('jwt') || localStorage.getItem('access_token') || ''; }
|
||||||
@@ -38,7 +70,11 @@
|
|||||||
}
|
}
|
||||||
function logout(){
|
function logout(){
|
||||||
if(!confirm('Odjava iz aplikacije?')) return;
|
if(!confirm('Odjava iz aplikacije?')) return;
|
||||||
try { localStorage.removeItem('jwt'); localStorage.removeItem('access_token'); localStorage.removeItem('app-role'); } catch(e){}
|
try {
|
||||||
|
localStorage.removeItem('jwt');
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('app-role');
|
||||||
|
} catch(e){}
|
||||||
location.href = '/sport/login';
|
location.href = '/sport/login';
|
||||||
}
|
}
|
||||||
function initials(n){
|
function initials(n){
|
||||||
@@ -46,55 +82,79 @@
|
|||||||
const p = String(n).trim().split(/\s+/);
|
const p = String(n).trim().split(/\s+/);
|
||||||
return ((p[0]||'')[0]||'').toUpperCase() + ((p[1]||'')[0]||'').toUpperCase();
|
return ((p[0]||'')[0]||'').toUpperCase() + ((p[1]||'')[0]||'').toUpperCase();
|
||||||
}
|
}
|
||||||
function esc(s){
|
|
||||||
return String(s==null?'':s).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
|
// Determine current page intent
|
||||||
|
function getActiveKey(){
|
||||||
|
const cs = document.currentScript || Array.from(document.scripts).find(s => /sidebar\.js/.test(s.src||''));
|
||||||
|
return (cs && cs.dataset && cs.dataset.active) || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to read /api/auth/me for footer display (best effort)
|
function userCanSeeSection(sec, user){
|
||||||
async function tryLoadMe(){
|
if(!sec.requireRole || !sec.requireRole.length) return true;
|
||||||
const tok = readToken(); if(!tok) return null;
|
if(!user) return false;
|
||||||
|
return sec.requireRole.indexOf(user.user_type) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSections(activeKey, user){
|
||||||
|
const out = [];
|
||||||
|
for(const sec of SIDEBAR_SECTIONS){
|
||||||
|
if(!userCanSeeSection(sec, user)) continue;
|
||||||
|
out.push(`<div class="pgz-sb-section ${sec.title==='ADMIN'?'pgz-admin':''}">${esc(sec.title)}</div>`);
|
||||||
|
for(const it of sec.items){
|
||||||
|
const active = it.id === activeKey;
|
||||||
|
const cross = it.href && it.href.startsWith('/sport/') && !sameOrigin(it.href);
|
||||||
|
out.push(`<a class="pgz-nav-i ${active?'active':''} ${cross?'cross-portal':''}"
|
||||||
|
href="${esc(it.href)}" data-id="${esc(it.id)}" data-label="${esc(it.label)}">
|
||||||
|
<span class="ic">${it.ic||'•'}</span>
|
||||||
|
<span class="lbl">${esc(it.label)}</span>
|
||||||
|
${it.badge?`<span class="badge">${esc(it.badge)}</span>`:''}
|
||||||
|
</a>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// is this href same as current page (just hash navigation)?
|
||||||
|
function sameOrigin(href){
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/sport/api/auth/me', {headers:{'Authorization':'Bearer '+tok}});
|
const u = new URL(href, location.href);
|
||||||
if(!r.ok) return null;
|
return u.origin === location.origin && u.pathname === location.pathname;
|
||||||
return await r.json();
|
} catch(e){ return false; }
|
||||||
} catch(e){ return null; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderShell(activeKey, internalNavHTML){
|
function renderShell(activeKey, user){
|
||||||
const sb = document.createElement('aside');
|
const sb = document.createElement('aside');
|
||||||
sb.id = 'pgz-sb';
|
sb.id = 'pgz-sb';
|
||||||
sb.innerHTML = `
|
sb.innerHTML = `
|
||||||
<div class="pgz-sb-h">
|
<div class="pgz-sb-h">
|
||||||
|
<div class="pgz-mark">PGŽ</div>
|
||||||
|
<div class="pgz-htxt">
|
||||||
<div class="pgz-logo">PGŽ <span class="g">SPORT</span></div>
|
<div class="pgz-logo">PGŽ <span class="g">SPORT</span></div>
|
||||||
<div class="pgz-sub">Operativna platforma</div>
|
<div class="pgz-sub">Odjel za sport</div>
|
||||||
<div class="pgz-sb-toggle" onclick="PGZSidebar.toggle()" title="Skupi/raširi (≡)">≡</div>
|
</div>
|
||||||
|
<div class="pgz-sb-toggle" onclick="PGZSidebar.toggle()" title="Skupi/raširi">←</div>
|
||||||
<div class="pgz-sb-mx" onclick="PGZSidebar.closeMobile()" title="Zatvori">✕</div>
|
<div class="pgz-sb-mx" onclick="PGZSidebar.closeMobile()" title="Zatvori">✕</div>
|
||||||
</div>
|
</div>
|
||||||
${internalNavHTML ? `<div class="pgz-sb-sep">Sekcije</div>` : ''}
|
<nav class="pgz-sb-nav" id="pgz-sb-nav">${renderSections(activeKey, user)}</nav>
|
||||||
<nav class="pgz-sb-nav" id="pgz-sb-nav">
|
<div class="pgz-sb-foot" id="pgz-sb-foot" onclick="PGZSidebar.toggleUserMenu(event)">
|
||||||
${internalNavHTML || ''}
|
|
||||||
<div class="pgz-sb-sep" id="pgz-portal-sep">Portali</div>
|
|
||||||
<div id="pgz-portal-mount">${renderExternal(activeKey)}</div>
|
|
||||||
</nav>
|
|
||||||
<div class="pgz-sb-foot" id="pgz-sb-foot">
|
|
||||||
<div class="av" id="pgz-sb-av">PG</div>
|
<div class="av" id="pgz-sb-av">PG</div>
|
||||||
<div class="ui">
|
<div class="ui">
|
||||||
<div class="un" id="pgz-sb-un">Gost</div>
|
<div class="un" id="pgz-sb-un">Gost</div>
|
||||||
<div class="ur" id="pgz-sb-ur">Demo</div>
|
<div class="ur" id="pgz-sb-ur">Demo</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lo" onclick="PGZSidebar.logout()" title="Odjava">⎋</div>
|
<div class="caret">▾</div>
|
||||||
|
<div class="pgz-user-menu" id="pgz-user-menu" onclick="event.stopPropagation()">
|
||||||
|
<a href="/sport/app#profil"><span>👤</span><span>Moj profil</span></a>
|
||||||
|
<a href="/sport/app#postavke"><span>⚙</span><span>Postavke</span></a>
|
||||||
|
<a href="/sport/static/sport2.html"><span>🌐</span><span>Public portal</span></a>
|
||||||
|
<div class="sep"></div>
|
||||||
|
<a href="/sport/login" id="pgz-menu-login"><span>🔑</span><span>Prijava</span></a>
|
||||||
|
<a class="danger" id="pgz-menu-logout" onclick="PGZSidebar.logout()" style="display:none"><span>⎋</span><span>Odjava</span></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return sb;
|
return sb;
|
||||||
}
|
}
|
||||||
function renderExternal(activeKey){
|
|
||||||
return NAV_EXTERNAL.map(n => `
|
|
||||||
<a class="pgz-nav-i pgz-nav-ext ${n.id===activeKey?'active':''}"
|
|
||||||
href="${n.href}" data-id="${n.id}" data-label="${esc(n.label)}">
|
|
||||||
<span class="ic">${n.ic}</span>
|
|
||||||
<span class="lbl">${esc(n.label)}</span>
|
|
||||||
</a>`).join('');
|
|
||||||
}
|
|
||||||
function renderBurger(){
|
function renderBurger(){
|
||||||
if(document.getElementById('pgz-sb-burger')) return;
|
if(document.getElementById('pgz-sb-burger')) return;
|
||||||
const b = document.createElement('div');
|
const b = document.createElement('div');
|
||||||
@@ -106,22 +166,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setUserDisplay(me){
|
function setUserDisplay(me){
|
||||||
|
const un = $('#pgz-sb-un'), ur = $('#pgz-sb-ur'), av = $('#pgz-sb-av');
|
||||||
|
const loginLink = document.getElementById('pgz-menu-login');
|
||||||
|
const logoutLink = document.getElementById('pgz-menu-logout');
|
||||||
if(!me){
|
if(!me){
|
||||||
$('#pgz-sb-un') && ($('#pgz-sb-un').textContent = 'Gost');
|
if(un) un.textContent = 'Gost';
|
||||||
$('#pgz-sb-ur') && ($('#pgz-sb-ur').textContent = 'Demo · click Prijava');
|
if(ur) ur.textContent = 'Klikni za prijavu';
|
||||||
$('#pgz-sb-av') && ($('#pgz-sb-av').textContent = '?');
|
if(av){ av.textContent = '?'; av.innerHTML = av.innerHTML; }
|
||||||
|
if(loginLink) loginLink.style.display = 'flex';
|
||||||
|
if(logoutLink) logoutLink.style.display = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const name = me.full_name || ((me.ime||'')+' '+(me.prezime||'')).trim() || me.email || '—';
|
const name = me.full_name || ((me.ime||'')+' '+(me.prezime||'')).trim() || me.email || '—';
|
||||||
const role = me.user_type || '';
|
const role = me.user_type || '';
|
||||||
const av = me.avatar_url || me.google_picture;
|
const avSrc = me.avatar_url || me.google_picture;
|
||||||
if($('#pgz-sb-un')) $('#pgz-sb-un').textContent = name;
|
if(un) un.textContent = name;
|
||||||
if($('#pgz-sb-ur')) $('#pgz-sb-ur').textContent = role;
|
if(ur) ur.textContent = role;
|
||||||
const avEl = $('#pgz-sb-av');
|
if(av){
|
||||||
if(avEl){
|
if(avSrc) av.innerHTML = `<img src="${esc(avSrc)}" alt="">`;
|
||||||
if(av) avEl.innerHTML = `<img src="${esc(av)}" alt="">`;
|
else av.textContent = initials(name);
|
||||||
else avEl.textContent = initials(name);
|
|
||||||
}
|
}
|
||||||
|
if(loginLink) loginLink.style.display = 'none';
|
||||||
|
if(logoutLink) logoutLink.style.display = 'flex';
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyCollapsedFromStorage(){
|
function applyCollapsedFromStorage(){
|
||||||
@@ -133,39 +199,42 @@
|
|||||||
document.body.classList.toggle('pgz-sb-col', col);
|
document.body.classList.toggle('pgz-sb-col', col);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function tryLoadMe(){
|
||||||
|
const tok = readToken(); if(!tok) return null;
|
||||||
|
try {
|
||||||
|
const r = await fetch('/sport/api/auth/me', {headers:{'Authorization':'Bearer '+tok}});
|
||||||
|
if(!r.ok) return null;
|
||||||
|
return await r.json();
|
||||||
|
} catch(e){ return null; }
|
||||||
|
}
|
||||||
|
|
||||||
// ────────── Public API ──────────
|
// ────────── Public API ──────────
|
||||||
const PGZSidebar = {
|
const PGZSidebar = {
|
||||||
NAV_EXTERNAL,
|
SIDEBAR_SECTIONS,
|
||||||
|
|
||||||
// Render: insert sidebar shell at document start; if a page provides internalNavHTML, use it
|
async mount(opts){
|
||||||
mount(opts){
|
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
const activeKey = opts.activeKey || (document.currentScript && document.currentScript.dataset.active) || '';
|
const activeKey = opts.activeKey || getActiveKey();
|
||||||
const internalNavHTML = opts.internalNavHTML || '';
|
|
||||||
// Skip mount if the page already has its own sidebar AND a portal mount point is provided
|
|
||||||
if(opts.skipShell){
|
|
||||||
const mount = document.getElementById('pgz-portal-mount');
|
|
||||||
if(mount){ mount.innerHTML = renderExternal(activeKey); }
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const existing = document.getElementById('pgz-sb');
|
const existing = document.getElementById('pgz-sb');
|
||||||
if(existing) existing.remove();
|
if(existing) existing.remove();
|
||||||
const sb = renderShell(activeKey, internalNavHTML);
|
|
||||||
|
// Try /api/auth/me first so ADMIN section is gated correctly
|
||||||
|
const me = await tryLoadMe();
|
||||||
|
const sb = renderShell(activeKey, me);
|
||||||
document.body.insertBefore(sb, document.body.firstChild);
|
document.body.insertBefore(sb, document.body.firstChild);
|
||||||
document.body.classList.add('pgz-has-sb');
|
document.body.classList.add('pgz-has-sb');
|
||||||
renderBurger();
|
renderBurger();
|
||||||
applyCollapsedFromStorage();
|
applyCollapsedFromStorage();
|
||||||
tryLoadMe().then(setUserDisplay);
|
setUserDisplay(me);
|
||||||
},
|
|
||||||
|
|
||||||
// Append portal links to an existing custom sidebar (call this from a page's own buildNav)
|
// listen for hashchange to update active item without reload
|
||||||
appendPortalLinksTo(navEl, activeKey){
|
window.addEventListener('hashchange', () => {
|
||||||
if(!navEl) return;
|
const h = location.hash.replace(/^#/,'');
|
||||||
activeKey = activeKey || '';
|
if(!h) return;
|
||||||
navEl.insertAdjacentHTML('beforeend',
|
document.querySelectorAll('#pgz-sb .pgz-nav-i').forEach(el => {
|
||||||
'<div class="pgz-sb-sep" style="padding:14px 14px 4px 14px;font-size:9.5px;color:var(--t4,#4e5a7a);text-transform:uppercase;letter-spacing:1.2px;font-weight:700">Portali</div>'
|
el.classList.toggle('active', el.dataset.id===h);
|
||||||
);
|
});
|
||||||
navEl.insertAdjacentHTML('beforeend', renderExternal(activeKey));
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
toggle(){
|
toggle(){
|
||||||
@@ -180,7 +249,6 @@
|
|||||||
if(!sb) return;
|
if(!sb) return;
|
||||||
sb.classList.add('pgz-mobile-open');
|
sb.classList.add('pgz-mobile-open');
|
||||||
document.body.classList.add('pgz-mobile-sb-open');
|
document.body.classList.add('pgz-mobile-sb-open');
|
||||||
// close on backdrop click
|
|
||||||
const closer = (ev) => {
|
const closer = (ev) => {
|
||||||
if(!sb.contains(ev.target) && ev.target.id !== 'pgz-sb-burger'){
|
if(!sb.contains(ev.target) && ev.target.id !== 'pgz-sb-burger'){
|
||||||
PGZSidebar.closeMobile();
|
PGZSidebar.closeMobile();
|
||||||
@@ -195,20 +263,36 @@
|
|||||||
sb.classList.remove('pgz-mobile-open');
|
sb.classList.remove('pgz-mobile-open');
|
||||||
document.body.classList.remove('pgz-mobile-sb-open');
|
document.body.classList.remove('pgz-mobile-sb-open');
|
||||||
},
|
},
|
||||||
|
toggleUserMenu(ev){
|
||||||
|
ev && ev.stopPropagation();
|
||||||
|
const f = document.getElementById('pgz-sb-foot');
|
||||||
|
const m = document.getElementById('pgz-user-menu');
|
||||||
|
if(!f || !m) return;
|
||||||
|
const open = f.classList.toggle('menu-open');
|
||||||
|
m.classList.toggle('open', open);
|
||||||
|
if(open){
|
||||||
|
const closer = (e) => {
|
||||||
|
if(!f.contains(e.target)){
|
||||||
|
f.classList.remove('menu-open');
|
||||||
|
m.classList.remove('open');
|
||||||
|
document.removeEventListener('click', closer, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setTimeout(() => document.addEventListener('click', closer, true), 50);
|
||||||
|
}
|
||||||
|
},
|
||||||
logout
|
logout
|
||||||
};
|
};
|
||||||
window.PGZSidebar = PGZSidebar;
|
window.PGZSidebar = PGZSidebar;
|
||||||
|
|
||||||
// Auto-mount unless data-inline=1
|
// Auto-mount unless data-inline=1
|
||||||
function autoMount(){
|
const cs = document.currentScript;
|
||||||
const cs = document.currentScript || Array.from(document.scripts).find(s => /sidebar\.js/.test(s.src||''));
|
|
||||||
const inline = cs && cs.dataset && cs.dataset.inline === '1';
|
const inline = cs && cs.dataset && cs.dataset.inline === '1';
|
||||||
if(inline) return; // page will call PGZSidebar.mount() itself
|
if(!inline){
|
||||||
if(document.readyState === 'loading'){
|
if(document.readyState === 'loading'){
|
||||||
document.addEventListener('DOMContentLoaded', () => PGZSidebar.mount({}));
|
document.addEventListener('DOMContentLoaded', () => PGZSidebar.mount({}));
|
||||||
} else {
|
} else {
|
||||||
PGZSidebar.mount({});
|
PGZSidebar.mount({});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
autoMount();
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
+12
-15
@@ -32,7 +32,9 @@ button,input,select{font-family:inherit;font-size:inherit;outline:none}
|
|||||||
::-webkit-scrollbar-thumb:hover{background:var(--pgz-blue2)}
|
::-webkit-scrollbar-thumb:hover{background:var(--pgz-blue2)}
|
||||||
|
|
||||||
.app{display:flex;min-height:100vh}
|
.app{display:flex;min-height:100vh}
|
||||||
.sb{width:240px;background:linear-gradient(180deg,var(--bg1) 0%,var(--bg0) 100%);border-right:1px solid var(--rim);position:fixed;top:0;left:0;bottom:0;display:flex;flex-direction:column;z-index:10;transition:width .22s ease}
|
/* Native sidebar replaced by shared /static/shared/sidebar.* — kept hidden but DOM intact for legacy buildNav() calls */
|
||||||
|
.sb{display:none}
|
||||||
|
.sb-old{width:240px;background:linear-gradient(180deg,var(--bg1) 0%,var(--bg0) 100%);border-right:1px solid var(--rim);position:fixed;top:0;left:0;bottom:0;display:flex;flex-direction:column;z-index:10;transition:width .22s ease}
|
||||||
.sb-h{padding:18px 18px 14px;border-bottom:1px solid var(--rim);position:relative}
|
.sb-h{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{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 .logo .g{color:var(--pgz-gold)}
|
||||||
@@ -63,8 +65,8 @@ button,input,select{font-family:inherit;font-size:inherit;outline: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-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}
|
.sb.collapsed .nav-ext span:last-child{display:none}
|
||||||
|
|
||||||
.main{margin-left:240px;flex:1;min-width:0;transition:margin-left .22s ease}
|
.main{margin-left:0;flex:1;min-width:0;transition:margin-left .22s ease}
|
||||||
.sb.collapsed ~ .main{margin-left:58px}
|
/* body.pgz-has-sb (from shared/sidebar.css) provides the left padding */
|
||||||
.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}
|
.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}
|
||||||
.tb-t{font-size:15px;font-weight:700;color:var(--t0)}
|
.tb-t{font-size:15px;font-weight:700;color:var(--t0)}
|
||||||
.tb-s{font-size:11px;color:var(--t2)}
|
.tb-s{font-size:11px;color:var(--t2)}
|
||||||
@@ -222,6 +224,8 @@ a.tag:hover,.tag[onclick]:hover{transform:translateY(-1px);filter:brightness(1.1
|
|||||||
.pp-stats{grid-template-columns:repeat(3,1fr)}
|
.pp-stats{grid-template-columns:repeat(3,1fr)}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<link rel="stylesheet" href="/sport/static/shared/sidebar.css">
|
||||||
|
<script src="/sport/static/shared/sidebar.js" defer data-active="dashboard"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -572,22 +576,15 @@ function closePanel(){
|
|||||||
document.addEventListener('keydown', e => { if(e.key==='Escape') closePanel(); });
|
document.addEventListener('keydown', e => { if(e.key==='Escape') closePanel(); });
|
||||||
|
|
||||||
//=========== NAVIGATION ===========
|
//=========== NAVIGATION ===========
|
||||||
const NAV_EXTERNAL = [
|
|
||||||
{id:'login', href:'/sport/login', ic:'\u{1F511}', label:'Prijava'},
|
|
||||||
{id:'app', href:'/sport/app', ic:'\u{1F4F1}', label:'Aplikacija'},
|
|
||||||
{id:'admin', href:'/sport/admin', ic:'\u{1F6E1}', label:'Administracija'},
|
|
||||||
{id:'crm', href:'/sport/crm', ic:'\u{1F465}', label:'CRM'},
|
|
||||||
{id:'erp', href:'/sport/erp', ic:'\u{1F4B0}', label:'ERP'},
|
|
||||||
{id:'kpi', href:'/sport/kpi', ic:'\u{1F4C8}', label:'KPI'},
|
|
||||||
{id:'audit', href:'/sport/audit', ic:'\u{1F4CB}', label:'Audit'}
|
|
||||||
];
|
|
||||||
function buildNav(){
|
function buildNav(){
|
||||||
const nav = $('#nav');
|
const nav = $('#nav');
|
||||||
nav.innerHTML = NAV_ITEMS.map(n => '<div class="nav-i '+(n.id===_state.section?'active':'')+'" data-id="'+n.id+'" data-label="'+n.label+'" onclick="navTo(\''+n.id+'\')"><span class="ic">'+n.ic+'</span><span class="lbl">'+n.label+'</span></div>').join('');
|
nav.innerHTML = NAV_ITEMS.map(n => '<div class="nav-i '+(n.id===_state.section?'active':'')+'" data-id="'+n.id+'" data-label="'+n.label+'" onclick="navTo(\''+n.id+'\')"><span class="ic">'+n.ic+'</span><span class="lbl">'+n.label+'</span></div>').join('');
|
||||||
// PORTALI section + external links
|
|
||||||
nav.innerHTML += '<div class="nav-sep" style="padding:14px 14px 4px 14px;font-size:9.5px;color:var(--t4);text-transform:uppercase;letter-spacing:1.2px;font-weight:700;white-space:nowrap;overflow:hidden">Portali</div>';
|
|
||||||
nav.innerHTML += NAV_EXTERNAL.map(n => '<a class="nav-i nav-ext" href="'+n.href+'" data-id="'+n.id+'" data-label="'+esc(n.label)+'" style="color:var(--cyan);text-decoration:none"><span class="ic">'+n.ic+'</span><span class="lbl">'+esc(n.label)+'</span><span style="margin-left:auto;font-size:10px;opacity:.5">↗</span></a>').join('');
|
|
||||||
}
|
}
|
||||||
|
// Hashchange handler — accept routing from unified shared sidebar
|
||||||
|
window.addEventListener('hashchange', () => {
|
||||||
|
const h = (location.hash||'').replace(/^#/,'');
|
||||||
|
if(h && NAV_ITEMS.some(n => n.id===h)) navTo(h);
|
||||||
|
});
|
||||||
function toggleSidebar(){
|
function toggleSidebar(){
|
||||||
const sb = document.getElementById('sb');
|
const sb = document.getElementById('sb');
|
||||||
const tg = document.getElementById('sb-toggle');
|
const tg = document.getElementById('sb-toggle');
|
||||||
|
|||||||
Reference in New Issue
Block a user