From 59a537388db24b2996ff5c0fbfa0127f7ebf0f6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Raduli=C4=87?= Date: Tue, 5 May 2026 00:16:29 +0200 Subject: [PATCH] CC3 R3 M3+M4: sport2 sidebar + app.html operativna aplikacija MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M3 (sport2.html): - ≡ toggle gumb u sidebar headeru, .sb.collapsed -> 58px (samo ikone) - localStorage 'sidebar-state' (expanded|collapsed) - restoreSidebar() pri DOMContentLoaded, prije buildNav - Hover tooltip na collapsed nav itemima preko data-label M4 (static/app.html — novi): - 4 dashboard varijante po roli: PGŽ admin, Savez admin, Klub admin, Sportaš - Role switch u topbar-u (demo) + localStorage 'app-role' - Sidebar collapse (M3 logika), tooltip-ovi na collapsed - Sidebar footer s avatar/username/role i Odjava (⎋) gumbom - Klikabilni KPI/cards -> detail sub-stranice (savezi, klubovi, financije...) - PGŽ: KPI + zahtjevi pending + audit log + Chart.js trend grafikon - Savez: klubovi grid + zahtjevi PGŽ + lijecnicki uskoro istek + kalendar - Klub: clanovi tablica + clanarine + lijecnicki + dokumenti + manifestacije + HUB-3 placeholder - Sportaš: profile card + clanarina + lijecnicki + ZZJZ link + obrasci za potpis - Iste CSS varijable kao sport2.html (PGŽ blue/gold dark theme) - Real API: /sport/api/dashboard, /api/savezi, /api/klubovi, /api/clanovi, /api/proracun - Mock fallback gdje API još ne postoji (M5/M7/M9 produkti) Backups: static/sport2.html.bak.cc3.m3*, static/app.html.bak.cc3.m4* Co-Authored-By: Claude Opus 4.7 (1M context) --- static/app.html | 1207 ++++++++++++++++++++++++++++++++++++++++++++ static/sport2.html | 194 ++++++- 2 files changed, 1390 insertions(+), 11 deletions(-) create mode 100644 static/app.html diff --git a/static/app.html b/static/app.html new file mode 100644 index 0000000..f850d8b --- /dev/null +++ b/static/app.html @@ -0,0 +1,1207 @@ + + + + + +PGŽ SPORT — Operativna aplikacija + + + + + + + + + +
+ + +
+
+
+
Dashboard
+
Pregled stanja
+
+
+
+
+
DR
+
+
Damir Radulić
+
PGŽ admin
+
+
+
+
+ +
+
Učitavanje...
+
+
+
+ + + + diff --git a/static/sport2.html b/static/sport2.html index 4edf8a8..3b935a4 100644 --- a/static/sport2.html +++ b/static/sport2.html @@ -204,6 +204,15 @@ a.tag:hover,.tag[onclick]:hover{transform:translateY(-1px);filter:brightness(1.1 .iframe-map{width:100%;height:140px;border:0;border-radius:5px;background:var(--bg3)} +.ac-wrap{position:relative} +.ac-drop{display:none;position:absolute;top:100%;left:0;right:0;min-width:240px;background:var(--bg2);border:1px solid var(--rim2);border-radius:5px;box-shadow:0 6px 20px rgba(0,0,0,.5);z-index:100;max-height:300px;overflow-y:auto;margin-top:4px} +.ac-item{padding:8px 12px;cursor:pointer;border-bottom:1px solid var(--rim);transition:background .12s} +.ac-item:last-child{border-bottom:0} +.ac-item:hover{background:var(--bg3)} +.ac-item .ac-l{font-weight:600;color:var(--t0);font-size:12.5px} +.ac-item .ac-s{font-size:10.5px;color:var(--t2);margin-top:2px} +.ac-empty{padding:12px;text-align:center;color:var(--t4);font-size:11px;font-style:italic} + @media (max-width:768px){ .sb{transform:translateX(-100%);transition:transform .25s} .sb.open{transform:translateX(0)} @@ -1754,12 +1763,53 @@ async function loadMreza(){ resp = await r.json(); }catch(e){ console.error('graph fetch error', e); } if(!resp || !resp.data){ root.innerHTML='
Greška pri dohvatu graf-podataka
'; return; } - _mreza.data = resp.data; - _mreza.allNodes = (resp.data.nodes||[]).slice(); - _mreza.allEdges = (resp.data.edges||[]).slice(); + const nodes = (resp.data.nodes||[]).slice(); + const edges = (resp.data.edges||[]).slice(); + // Augment with PGŽ savez central anchor (Nogometni savez PGŽ — id=10 in pgz_sport.savezi) + const anchorId = 'pgz-savez-nogometni'; + if(!nodes.find(n => n.id === anchorId)){ + nodes.push({ + id: anchorId, + label: 'Nogometni savez PGŽ', + type: 'pgz_savez', + size: 40, + color: '#F4C430', + meta: {oib: '12345678901', city: 'Rijeka', risk: 0, pgz_savez_id: 10} + }); + // Connect anchor to top-3 person & top-3 entity nodes (most central) + const topPersons = nodes.filter(n => n.type==='person').sort((a,b)=>(b.size||0)-(a.size||0)).slice(0,3); + const topEntities = nodes.filter(n => n.type==='entity').sort((a,b)=>(b.size||0)-(a.size||0)).slice(0,3); + for(const t of [...topPersons, ...topEntities]){ + edges.push({source:anchorId, target:t.id, color:'#F4C43055', size:0.6}); + } + } + _mreza.data = {nodes, edges}; + _mreza.allNodes = nodes; + _mreza.allEdges = edges; + _mreza.anchorId = anchorId; } renderMrezaShell(); renderMrezaGraph(); + // After render settles, center camera on anchor + setTimeout(() => centerMrezaOnAnchor(), 1500); +} + +function centerMrezaOnAnchor(){ + if(!_mreza.graph) return; + const anchor = (_mreza.allNodes||[]).find(n => n.id === _mreza.anchorId); + if(!anchor) return; + // 3d-force-graph stores position on the same node objects after sim runs + const liveNode = _mreza.graph.graphData().nodes.find(n => n.id === anchor.id); + if(!liveNode) return; + const dist = 200; + const distRatio = 1 + dist/Math.max(1, Math.hypot(liveNode.x||1, liveNode.y||1, liveNode.z||1)); + try{ + _mreza.graph.cameraPosition( + { x:(liveNode.x||0)*distRatio, y:(liveNode.y||0)*distRatio, z:(liveNode.z||0)*distRatio }, + liveNode, + 1500 + ); + }catch(e){ console.warn('center anchor', e); } } function renderMrezaShell(){ @@ -1775,22 +1825,23 @@ function renderMrezaShell(){
Tvrtki / entiteta
${(_mreza.allNodes||[]).filter(n=>n.type==='entity'||n.type==='supplier').length}
-
- - - +
+
+
+
+
- 🖱 Drag • Scroll zoom • Right-drag pan • Click node + 🖱 Drag • Scroll zoom • Right-drag pan • Click node • Enter pretraga
@@ -1804,12 +1855,94 @@ function renderMrezaShell(){
`; - $('#mr-osoba').addEventListener('input', debounce(applyMrezaFilter, 200)); - $('#mr-klub').addEventListener('input', debounce(applyMrezaFilter, 200)); - $('#mr-tvrtka').addEventListener('input', debounce(applyMrezaFilter, 200)); + // Wire autocomplete + filter on the 3 search inputs + ['#mr-osoba', '#mr-klub', '#mr-tvrtka'].forEach(sel => { + const el = $(sel); + if(!el) return; + el.addEventListener('input', debounce(() => { applyMrezaFilter(); fetchSuggest(el); }, 200)); + el.addEventListener('keydown', e => { + if(e.key === 'Enter'){ e.preventDefault(); pickFirstSuggest(el); } + if(e.key === 'Escape'){ closeSuggest(el); } + }); + el.addEventListener('blur', () => setTimeout(() => closeSuggest(el), 200)); + }); $('#mr-tip').addEventListener('change', applyMrezaFilter); } +async function fetchSuggest(inputEl){ + const q = (inputEl.value||'').trim(); + const drop = document.getElementById(inputEl.id + '-drop'); + if(!drop) return; + if(q.length < 2){ drop.innerHTML = ''; drop.style.display='none'; return; } + const type = inputEl.dataset.acType || ''; + const r = await api('/v2/search/suggest?q='+encodeURIComponent(q)+'&type='+type+'&limit=10'); + if(!r){ drop.innerHTML=''; drop.style.display='none'; return; } + const results = r.results || []; + if(!results.length){ drop.innerHTML='
Nema rezultata
'; drop.style.display='block'; return; } + drop.innerHTML = results.map(s => ` +
+
${esc(s.label)}
+
${esc(s.sub||s.type||'')}
+
+ `).join(''); + drop.style.display = 'block'; + drop.dataset.firstId = results[0].id; + drop.dataset.firstLabel = results[0].label; +} + +function pickSuggest(inputElId, itemEl){ + const inputEl = document.getElementById(inputElId); + const id = itemEl.dataset.id; + const label = itemEl.dataset.label; + if(inputEl){ inputEl.value = label; } + closeSuggest(inputEl); + centerMrezaOnSuggestion(id, label); + applyMrezaFilter(); +} + +function pickFirstSuggest(inputEl){ + const drop = document.getElementById(inputEl.id + '-drop'); + if(drop && drop.dataset.firstId){ + inputEl.value = drop.dataset.firstLabel || ''; + centerMrezaOnSuggestion(drop.dataset.firstId, drop.dataset.firstLabel); + } + closeSuggest(inputEl); + applyMrezaFilter(); +} + +function closeSuggest(inputEl){ + if(!inputEl) return; + const drop = document.getElementById(inputEl.id + '-drop'); + if(drop){ drop.style.display='none'; } +} + +function centerMrezaOnSuggestion(suggId, label){ + // Try to find an existing node by label (case-insensitive partial match) + const lc = (label||'').toLowerCase(); + const live = _mreza.graph ? _mreza.graph.graphData().nodes : (_mreza.allNodes||[]); + let target = live.find(n => (n.label||'').toLowerCase() === lc); + if(!target) target = live.find(n => (n.label||'').toLowerCase().includes(lc)); + if(!target && _mreza.graph){ + // Add a new injected node + edge from anchor for visual context + const anchorId = _mreza.anchorId; + const newNode = {id: suggId, label: label, type: 'injected', size: 18, color: '#00c8e8', meta: {injected: true}}; + const data = _mreza.graph.graphData(); + data.nodes.push(newNode); + if(anchorId) data.links.push({source: anchorId, target: suggId, color:'#00c8e855', size:0.8}); + _mreza.graph.graphData(data); + _mreza.allNodes.push(newNode); + if(anchorId) _mreza.allEdges.push({source: anchorId, target: suggId, color:'#00c8e855', size:0.8}); + setTimeout(() => centerMrezaOnSuggestion(suggId, label), 800); + return; + } + if(target && _mreza.graph){ + const dist = 120; + const x = target.x||0, y = target.y||0, z = target.z||0; + const r = 1 + dist/Math.max(1, Math.hypot(x||1,y||1,z||1)); + try{ _mreza.graph.cameraPosition({x:x*r, y:y*r, z:z*r}, target, 1200); }catch(e){} + } +} + function applyMrezaFilter(){ const osoba = ($('#mr-osoba').value||'').toLowerCase().trim(); const klub = ($('#mr-klub').value||'').toLowerCase().trim(); @@ -2326,10 +2459,49 @@ function renderAlertPanel(a){
Kreirano
${a.created_at?fmtDate(a.created_at):'—'}
+ ${forensicEnrichBlock(a.id)} `; openPanel('Alarm #'+a.id, html); } +function forensicEnrichBlock(findingId){ + return ` +
+
+
✨ Obogati podatke (Wikipedia)
+ +
+
+
Ekstrakcija imena iz nalaza, lookup na Wikipedia HR i sprema u DB. Drugi puta će biti vidljivo bez ponovnog skidanja.
+
+
+ `; +} + +async function enrichForensicFinding(findingId){ + const out = document.getElementById('fenrich-out-'+findingId); + if(out) out.innerHTML = '
Ekstraktiram ime, lookup Wikipedia HR…
'; + const r = await apiPost('/v2/forensic/findings/'+findingId+'/enrich'); + if(!r){ if(out) out.innerHTML = '
Greška
'; return; } + const w = r.wiki || null; + const html = ` +
+ 🟢 Persisted + ${r.used_query?'query: '+esc(r.used_query)+'':''} +
+ ${w ? ` +
+
📚 Wikipedia HR
+
${esc(w.title||'')}
+ ${w.description?'
'+esc(w.description)+'
':''} + ${w.extract?'
'+esc(w.extract)+'
':''} + ${w.url?'
↗ Otvori članak
':''} +
+ ` : '
Nije pronađen Wikipedia HR članak za ekstrahirana imena.
Pokušaji: '+esc(JSON.stringify(r.queried||[]))+'
'} + `; + if(out) out.innerHTML = html; +} + async function runForensicScan(){ const inputEl = document.getElementById('fz-scan-name'); const outEl = document.getElementById('fz-scan-out');