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:
Damir Radulić
2026-05-05 01:42:16 +02:00
parent 7e674ad1ec
commit 3a79965899
7 changed files with 354 additions and 211 deletions
+18 -28
View File
@@ -36,7 +36,8 @@ button,input,select,textarea{font-family:inherit;font-size:inherit;outline:none}
/* ============ LAYOUT ============ */
.app{display:flex;min-height:100vh}
.sb{width:240px;background:linear-gradient(180deg,var(--bg1) 0%,var(--bg0) 100%);border-right:1px solid var(--rim);position:fixed;top:0;left:0;bottom:0;display:flex;flex-direction:column;z-index:10;transition:width .22s ease}
/* 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)}
@@ -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.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}
.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)}
@@ -256,6 +257,8 @@ table tbody tr:hover{background:var(--bg3)}
.role-switch{display:none}
}
</style>
<link rel="stylesheet" href="/sport/static/shared/sidebar.css">
<script src="/sport/static/shared/sidebar.js" defer data-active="profil"></script>
</head>
<body>
@@ -600,35 +603,22 @@ function setRole(r){
}
//=========== NAV ===========
const NAV_EXTERNAL = [
{id:'login', href:'/sport/login', ic:'\u{1F511}', label:'Prijava'},
{id:'app', href:'/sport/app', ic:'\u{1F4F1}', label:'Aplikacija'},
{id:'admin', href:'/sport/admin', ic:'\u{1F6E1}', label:'Administracija'},
{id:'crm', href:'/sport/crm', ic:'\u{1F465}', label:'CRM'},
{id:'erp', href:'/sport/erp', ic:'\u{1F4B0}', label:'ERP'},
{id:'kpi', href:'/sport/kpi', ic:'\u{1F4C8}', label:'KPI'},
{id:'audit', href:'/sport/audit', ic:'\u{1F4CB}', label:'Audit'},
{id:'sport2', href:'/sport/static/sport2.html', ic:'\u{1F310}', label:'Public portal'}
];
function buildNav(){
const items = NAV_BY_ROLE[_state.role] || [];
let html = items.map(n =>
$('#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('');
// 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){
_state.section = id;
$$('.nav-i').forEach(el => el.classList.toggle('active', el.dataset.id===id));
@@ -1158,7 +1148,7 @@ SECTIONS['pgz:racuni'] = () => `
SECTIONS['pgz:crm'] = () => `
<div style="margin-bottom:12px">
<a href="/sport/crm" target="_blank" class="btn primary" style="text-decoration:none">📋 Otvori CRM workspace (Članarine • Liječnički • Obrasci) — live API</a>
<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">
@@ -1255,7 +1245,7 @@ async function renderKalendar(opts){
const isToday = (k === today.toISOString().slice(0,10));
const evHtml = ev.slice(0,3).map(e => `<div style="font-size:10px;background:rgba(${e.color==='a'?'245,158,11':e.color==='b'?'26,115,232':'34,197,94'},0.18);padding:2px 4px;border-radius:3px;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(e.title)}${e.klub?' — '+esc(e.klub):''}">${esc(e.title.substring(0,28))}</div>`).join('');
const more = ev.length > 3 ? `<div style="font-size:9px;color:var(--t3);margin-top:2px">+${ev.length-3} više</div>` : '';
grid += `<div style="background:${isToday?'rgba(26,115,232,0.15)':'var(--bg2)'};border:1px solid ${isToday?'var(--pgz-blue)':'var(--rim)'};border-radius:6px;padding:6px;min-height:90px;${ev.length?'cursor:pointer':''}" ${ev.length?`onclick="alert('${esc(ev.map(x=>x.title+(x.klub?' — '+x.klub:'')).join('\\n').replace(/'/g,'\\\\\\'')\)}')"`:''}><div style="font-weight:600;font-size:13px;color:${isToday?'var(--pgz-blue)':'var(--t1)'}">${d}</div>${evHtml}${more}</div>`;
grid += `<div 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>';
@@ -1505,7 +1495,7 @@ SECTIONS['klub:clanovi'] = () => `
</div>`;
SECTIONS['klub:clanarine'] = () => `
<div style="margin-bottom:10px"><a href="/sport/crm" target="_blank" class="btn primary" style="text-decoration:none">📋 Otvori live CRM (HUB-3 PDF + EPC QR generator)</a></div>
<div 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>
@@ -1527,7 +1517,7 @@ SECTIONS['klub:clanarine'] = () => `
</div>`;
SECTIONS['klub:lijecnicki'] = () => `
<div style="margin-bottom:10px"><a href="/sport/crm" target="_blank" class="btn primary" style="text-decoration:none">⚕ Otvori live CRM — pregledi + ZZJZ PGŽ scheduling</a></div>
<div 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>
@@ -1626,7 +1616,7 @@ SECTIONS['sportas:clanarina'] = () => `
</div>`;
SECTIONS['sportas:lijecnicki'] = () => `
<div style="margin-bottom:10px"><a href="/sport/crm" target="_blank" class="btn primary" style="text-decoration:none">⚕ Otvori live CRM — pregledi + ZZJZ PGŽ scheduling</a></div>`+`
<div 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>
@@ -1658,7 +1648,7 @@ SECTIONS['sportas:dokumenti'] = () => `
</div>`;
SECTIONS['sportas:obrasci'] = () => `
<div style="margin-bottom:10px"><a href="/sport/crm" target="_blank" class="btn primary" style="text-decoration:none">📝 Otvori live obrasce — popuni i digitalno potpiši</a></div>
<div 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>