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
+34 -2
View File
@@ -136,7 +136,8 @@ table tr:hover td { background: rgba(26, 115, 232, 0.05); }
.toast.err { border-left-color: var(--err); }
</style>
<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>
<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="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="emailtpl" onclick="setTab('emailtpl')">📨 E-mail templates</div>
<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>
<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-stats" 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 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 === 'stats') loadStats();
if (name === 'notifs') loadNotifs();
if (name === 'emailtpl') loadEmailTpl();
}
// ════════════════════════════════════════════════════
@@ -294,7 +298,8 @@ async function loadClanarine() {
<div class="grow"></div>
<button class="btn" onclick="selectAllUnpaid()">☑ Sve nepladene</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>
</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">
@@ -381,6 +386,33 @@ async function doBulkNotify(body) {
} 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() {
const sel = getSelectedClanarine();
const body = sel.length ? {ids: sel.map(s => s.id)} : {};