π Audit Log
diff --git a/static/crm.html b/static/crm.html index da6bf61..4947eba 100644 --- a/static/crm.html +++ b/static/crm.html @@ -135,6 +135,8 @@ table tr:hover td { background: rgba(26, 115, 232, 0.05); } .toast.show { transform: translateX(0); } .toast.err { border-left-color: var(--err); } + + diff --git a/static/erp.html b/static/erp.html index 2854151..8a545a3 100644 --- a/static/erp.html +++ b/static/erp.html @@ -88,6 +88,16 @@ tr.clickable:hover { background:var(--bg-3); box-shadow:inset 3px 0 0 var(--acce + +Portali
+ πPrijava
+ π±Aplikacija
+ π‘Administracija
+ π₯CRM
+ π°ERP
+ πKPI
+ πAudit
+ πPublic portal
diff --git a/static/kpi.html b/static/kpi.html
index 2861b47..5f57fab 100644
--- a/static/kpi.html
+++ b/static/kpi.html
@@ -21,7 +21,10 @@
tr:hover { background: #1a2030; }
.updated { color: #678; font-size: 11px; }
.refresh { background: #4af; color: #fff; border: none; padding: 4px 12px; border-radius: 4px; cursor: pointer; }
+ body{padding:20px}
+
+
`;
+ else avEl.textContent = initials(name);
+ }
+ }
+
+ function applyCollapsedFromStorage(){
+ let col = false;
+ try { col = localStorage.getItem(STATE_KEY) === '1'; } catch(e){}
+ const sb = document.getElementById('pgz-sb');
+ if(!sb) return;
+ sb.classList.toggle('pgz-collapsed', col);
+ document.body.classList.toggle('pgz-sb-col', col);
+ }
+
+ // ββββββββββ Public API ββββββββββ
+ const PGZSidebar = {
+ NAV_EXTERNAL,
+
+ // Render: insert sidebar shell at document start; if a page provides internalNavHTML, use it
+ mount(opts){
+ opts = opts || {};
+ const activeKey = opts.activeKey || (document.currentScript && document.currentScript.dataset.active) || '';
+ 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');
+ if(existing) existing.remove();
+ const sb = renderShell(activeKey, internalNavHTML);
+ document.body.insertBefore(sb, document.body.firstChild);
+ document.body.classList.add('pgz-has-sb');
+ renderBurger();
+ applyCollapsedFromStorage();
+ tryLoadMe().then(setUserDisplay);
+ },
+
+ // Append portal links to an existing custom sidebar (call this from a page's own buildNav)
+ appendPortalLinksTo(navEl, activeKey){
+ if(!navEl) return;
+ activeKey = activeKey || '';
+ navEl.insertAdjacentHTML('beforeend',
+ '
RINET KPI Dashboard
diff --git a/static/login.html b/static/login.html index 8cfde49..4bd33b7 100644 --- a/static/login.html +++ b/static/login.html @@ -310,6 +310,8 @@ body { .cookie-actions button:hover { color: var(--text); border-color: var(--accent); } .cookie a { color: var(--accent); text-decoration: none; } + + diff --git a/static/shared/sidebar.css b/static/shared/sidebar.css new file mode 100644 index 0000000..0ed0118 --- /dev/null +++ b/static/shared/sidebar.css @@ -0,0 +1,148 @@ +/* PGΕ½ SPORT β Unified Sidebar v1.0 + * dradulic@outlook.com / damir@rinet.one β 2026-05-05 + * 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{ + --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; + --sb-w-exp:230px; --sb-w-col:58px; +} + +#pgz-sb{ + position:fixed; top:0; left:0; bottom:0; width:var(--sb-w-exp); + background:linear-gradient(180deg,var(--bg1) 0%,var(--bg0) 100%); + border-right:1px solid var(--rim); + display:flex; flex-direction:column; z-index:100; + font-family:'Inter',sans-serif; font-size:13px; color:var(--t1); + transition:width .22s ease, transform .22s ease; +} +#pgz-sb *{box-sizing:border-box} +#pgz-sb a{text-decoration:none;color:inherit} + +/* Header */ +.pgz-sb-h{padding:18px 18px 14px;border-bottom:1px solid var(--rim);position:relative;flex-shrink:0} +.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-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-toggle{ + position:absolute;top:14px;right:8px;width:24px;height:24px; + display:flex;align-items:center;justify-content:center;cursor:pointer; + color:var(--t2);background:var(--bg2);border:1px solid var(--rim); + border-radius:5px;font-size:14px;font-weight:700; + transition:all .15s;user-select:none; +} +.pgz-sb-toggle:hover{background:var(--bg3);color:var(--pgz-gold);border-color:var(--pgz-gold)} + +/* Section label / separator */ +.pgz-sb-sep{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} + +/* Nav */ +.pgz-sb-nav{flex:1;padding:6px 8px;overflow-y:auto;overflow-x:hidden} +.pgz-sb-nav::-webkit-scrollbar{width:6px} +.pgz-sb-nav::-webkit-scrollbar-thumb{background:var(--rim2);border-radius:3px} +.pgz-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;white-space:nowrap; + transition:background .15s,color .15s;position:relative; +} +.pgz-nav-i:hover{background:var(--bg2);color:var(--t1)} +.pgz-nav-i.active{ + background:linear-gradient(90deg,var(--pgz-blue) 0%,var(--pgz-blue2) 100%); + color:#fff;font-weight:600; +} +.pgz-nav-i .ic{width:20px;text-align:center;font-size:14px;flex-shrink: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-ext{color:var(--cyan)} +.pgz-nav-ext::after{content:"β";font-size:10px;opacity:.5;margin-left:auto;flex-shrink:0} +.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) */ +.pgz-sb-foot{padding:10px 12px;border-top:1px solid var(--rim); + display:flex;align-items:center;gap:8px; + white-space:nowrap;overflow:hidden;flex-shrink:0} +.pgz-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;overflow:hidden; +} +.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 .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 .lo{cursor:pointer;color:var(--t4);font-size:14px; + padding:6px 8px;border-radius:5px;transition:all .15s;flex-shrink:0} +.pgz-sb-foot .lo:hover{background:rgba(255,45,85,.15);color:var(--red)} + +/* Mobile burger (shown <768px when sidebar is offscreen) */ +.pgz-sb-burger{ + position:fixed;top:10px;left:10px;z-index:99; + width:36px;height:36px;display:none;align-items:center;justify-content:center; + background:var(--bg2);border:1px solid var(--rim);border-radius:6px; + color:var(--t1);font-size:18px;cursor:pointer; +} +.pgz-sb-burger:hover{background:var(--bg3);color:var(--pgz-gold)} + +/* Mobile X (shown <768px when sidebar is open) */ +.pgz-sb-mx{display:none;cursor:pointer;color:var(--t2);font-size:18px; + width:24px;height:24px;align-items:center;justify-content:center; + border-radius:5px;transition:all .15s} +.pgz-sb-mx:hover{background:var(--bg3);color:var(--red)} + +/* βββ Collapsed state βββ */ +#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 .pgz-logo{font-size:0} +#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-h .pgz-sub{display:none} +#pgz-sb.pgz-collapsed .pgz-sb-toggle{position:static;margin:6px auto 0;display:flex} +#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-nav-i .lbl, +#pgz-sb.pgz-collapsed .pgz-nav-i .badge, +#pgz-sb.pgz-collapsed .pgz-nav-ext::after{display:none} +#pgz-sb.pgz-collapsed .pgz-sb-foot{padding:10px 6px;justify-content:center} +#pgz-sb.pgz-collapsed .pgz-sb-foot .ui, +#pgz-sb.pgz-collapsed .pgz-sb-foot .lo{display:none} + +/* Tooltip when collapsed */ +#pgz-sb.pgz-collapsed .pgz-nav-i:hover::after{ + content:attr(data-label); + position:absolute;left:calc(var(--sb-w-col) - 4px);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);font-weight:600; + box-shadow:2px 2px 10px rgba(0,0,0,.45); + pointer-events:none;z-index:200; +} + +/* Layout helper β apply on body to push content right of sidebar */ +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)} + +/* Mobile: <768px */ +@media (max-width:768px){ + #pgz-sb{transform:translateX(-100%)} + #pgz-sb.pgz-mobile-open{transform:translateX(0)} + #pgz-sb.pgz-collapsed{width:var(--sb-w-exp)} /* full width on mobile when open */ + body.pgz-has-sb,body.pgz-has-sb.pgz-sb-col{padding-left:0} + .pgz-sb-burger{display:flex} + .pgz-sb-mx{display:flex} + .pgz-sb-toggle{display:none} + /* overlay backdrop */ + body.pgz-mobile-sb-open::before{ + content:"";position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:99;backdrop-filter:blur(2px) + } +} diff --git a/static/shared/sidebar.js b/static/shared/sidebar.js new file mode 100644 index 0000000..c1549dd --- /dev/null +++ b/static/shared/sidebar.js @@ -0,0 +1,214 @@ +/* PGΕ½ SPORT β Unified Sidebar v1.0 + * dradulic@outlook.com / damir@rinet.one β 2026-05-05 + * + * Usage on each page: + * + * // 0 (default) = render on load. 1 = call PGZSidebar.mount() yourself + * + * The script renders #pgz-sb at start of , adds class "pgz-has-sb" to body + * (so existing layouts can be migrated). Pages that already have their own sidebar + * should pass data-skip="1" β only NAV_EXTERNAL portal links will be appended to + * an element with id="pgz-portal-mount" if present. + */ +(function(){ + 'use strict'; + + // ββββββββββ Configuration ββββββββββ + // Per-portal "internal" sections (left as a hint; pages typically own their own internal nav) + // External portal links β same on every page + 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'} + ]; + + const STATE_KEY = 'sidebarCollapsed'; // shared across all pages + const $ = (s, root) => (root||document).querySelector(s); + + function readToken(){ + try { return localStorage.getItem('jwt') || localStorage.getItem('access_token') || ''; } + catch(e){ return ''; } + } + function logout(){ + if(!confirm('Odjava iz aplikacije?')) return; + try { localStorage.removeItem('jwt'); localStorage.removeItem('access_token'); localStorage.removeItem('app-role'); } catch(e){} + location.href = '/sport/login'; + } + function initials(n){ + if(!n) return '?'; + const p = String(n).trim().split(/\s+/); + return ((p[0]||'')[0]||'').toUpperCase() + ((p[1]||'')[0]||'').toUpperCase(); + } + function esc(s){ + return String(s==null?'':s).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); + } + + // Try to read /api/auth/me for footer display (best effort) + 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; } + } + + function renderShell(activeKey, internalNavHTML){ + const sb = document.createElement('aside'); + sb.id = 'pgz-sb'; + sb.innerHTML = ` +
+
+ ${internalNavHTML ? `PGΕ½ SPORT
+ Operativna platforma
+ β‘
+ β
+ Sekcije
` : ''}
+
+
+
+ `;
+ return sb;
+ }
+ function renderExternal(activeKey){
+ return NAV_EXTERNAL.map(n => `
+
+ ${n.ic}
+ ${esc(n.label)}
+ `).join('');
+ }
+ function renderBurger(){
+ if(document.getElementById('pgz-sb-burger')) return;
+ const b = document.createElement('div');
+ b.id = 'pgz-sb-burger';
+ b.className = 'pgz-sb-burger';
+ b.innerHTML = 'β‘';
+ b.onclick = () => PGZSidebar.openMobile();
+ document.body.appendChild(b);
+ }
+
+ function setUserDisplay(me){
+ if(!me){
+ $('#pgz-sb-un') && ($('#pgz-sb-un').textContent = 'Gost');
+ $('#pgz-sb-ur') && ($('#pgz-sb-ur').textContent = 'Demo Β· click Prijava');
+ $('#pgz-sb-av') && ($('#pgz-sb-av').textContent = '?');
+ return;
+ }
+ const name = me.full_name || ((me.ime||'')+' '+(me.prezime||'')).trim() || me.email || 'β';
+ const role = me.user_type || '';
+ const av = me.avatar_url || me.google_picture;
+ if($('#pgz-sb-un')) $('#pgz-sb-un').textContent = name;
+ if($('#pgz-sb-ur')) $('#pgz-sb-ur').textContent = role;
+ const avEl = $('#pgz-sb-av');
+ if(avEl){
+ if(av) avEl.innerHTML = `PG
+
+
+ Gost
+ Demo
+ β
+ Portali
'
+ );
+ navEl.insertAdjacentHTML('beforeend', renderExternal(activeKey));
+ },
+
+ toggle(){
+ const sb = document.getElementById('pgz-sb');
+ if(!sb) return;
+ const col = sb.classList.toggle('pgz-collapsed');
+ document.body.classList.toggle('pgz-sb-col', col);
+ try { localStorage.setItem(STATE_KEY, col ? '1' : '0'); } catch(e){}
+ },
+ openMobile(){
+ const sb = document.getElementById('pgz-sb');
+ if(!sb) return;
+ sb.classList.add('pgz-mobile-open');
+ document.body.classList.add('pgz-mobile-sb-open');
+ // close on backdrop click
+ const closer = (ev) => {
+ if(!sb.contains(ev.target) && ev.target.id !== 'pgz-sb-burger'){
+ PGZSidebar.closeMobile();
+ document.removeEventListener('click', closer, true);
+ }
+ };
+ setTimeout(() => document.addEventListener('click', closer, true), 50);
+ },
+ closeMobile(){
+ const sb = document.getElementById('pgz-sb');
+ if(!sb) return;
+ sb.classList.remove('pgz-mobile-open');
+ document.body.classList.remove('pgz-mobile-sb-open');
+ },
+ logout
+ };
+ window.PGZSidebar = PGZSidebar;
+
+ // Auto-mount unless data-inline=1
+ function autoMount(){
+ const cs = document.currentScript || Array.from(document.scripts).find(s => /sidebar\.js/.test(s.src||''));
+ const inline = cs && cs.dataset && cs.dataset.inline === '1';
+ if(inline) return; // page will call PGZSidebar.mount() itself
+ if(document.readyState === 'loading'){
+ document.addEventListener('DOMContentLoaded', () => PGZSidebar.mount({}));
+ } else {
+ PGZSidebar.mount({});
+ }
+ }
+ autoMount();
+})();
diff --git a/static/sport2.html b/static/sport2.html
index 29d27a0..8203a52 100644
--- a/static/sport2.html
+++ b/static/sport2.html
@@ -60,6 +60,8 @@ button,input,select{font-family:inherit;font-size:inherit;outline: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{font-size:0;padding:8px}
.sb.collapsed .sb-foot::before{content:"v2";font-size:9px;color:var(--t4)}
+.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}
.main{margin-left:240px;flex:1;min-width:0;transition:margin-left .22s ease}
.sb.collapsed ~ .main{margin-left:58px}
@@ -570,9 +572,21 @@ function closePanel(){
document.addEventListener('keydown', e => { if(e.key==='Escape') closePanel(); });
//=========== 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(){
const nav = $('#nav');
nav.innerHTML = NAV_ITEMS.map(n => '').join('');
+ // PORTALI section + external links
+ nav.innerHTML += '';
+ nav.innerHTML += NAV_EXTERNAL.map(n => ''+n.ic+''+esc(n.label)+'β').join('');
}
function toggleSidebar(){
const sb = document.getElementById('sb');