6-sub sprint: Dokumenti+HNS profil+Admin+ERP+CRM+PGŽ filter

SUB1 Dokumenti: pgz:dokumenti SECTIONS handler u app.html (klikabilan grid 19 godišnjaka, PDF stream)
SUB2 HNS profil: sport2.html drill-down — bio-chips (visina/težina/noga/poz/dres) + HNS deep + Google + Wiki + 🏆 Karijera/📅 Utakmice tabovi (Josip Zec id=449: 257 nast/182 gol/15 sez)
SUB3 Admin Users: sidebar.js href fix /admin/users → /sport/admin/users + razriješen audit ID konflikt
SUB4 ERP Full: 5 novih endpointa (invoice-uploads, racuni/ulazni/{rid}/uploads, expense-reports, putni-nalog-racuni, payments) + 3 nova taba (📎 Uploads/OCR, ✈ Putni, 💰 Plaćanja) + inline stavke drill-down + sidebar entry
SUB5 CRM Salesforce-Lite: dodan crm_v2 sidebar entry (router 956 linija već mounted)
SUB6 PGŽ filter: 2 nova endpointa /api/v2/savezi/priority-sort + /api/v2/clanovi/priority-sort; togglePGZFilter wired u Klubovi/Savezi/Sportaši (sport2.html + app.html); 💰📖 badge prefix; klubovi 1536/1641, savezi 35/246, sportaši 4979/5499

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Damir Radulić
2026-05-05 13:17:56 +02:00
parent 1d02c0897d
commit 16b980e842
6 changed files with 625 additions and 77 deletions
+94 -15
View File
@@ -1230,47 +1230,62 @@ SECTIONS['pgz:korisnici'] = () => {
};
SECTIONS['pgz:savezi'] = async () => {
const d = await api('/savezi') || {rows:[]};
const onPGZ = !!window._pgz_filter_priority;
const url = onPGZ ? '/v2/savezi/priority-sort?only=true&limit=300' : '/savezi';
const d = await api(url) || {rows:[]};
const top = (d.rows||[]).slice(0,30);
const bp = window.pgzBadgePrefix || (()=> '');
const rows = top.map(s => `
<tr style="cursor:pointer" onclick="showDetail('savez',${s.id},${JSON.stringify(s.naziv)})">
<td><b>${esc(s.naziv)}</b></td>
<td><b>${bp(s)}${esc(s.naziv)}</b></td>
<td class="num">${fmt(s.broj_klubova||'—')}</td>
<td class="num">${fmt(s.broj_sportasa||'—')}</td>
<td>${esc(s.predsjednik||'—')}</td>
<td><button class="btn sm" onclick="event.stopPropagation();showDetail('savez',${s.id},${JSON.stringify(s.naziv)})">Detalji</button></td>
</tr>`).join('');
return `<div class="card"><div class="card-h"><div class="card-t">🏅 Savezi PGŽ — top 30 (od ${d.count||246})</div></div>
const tb = window.renderPGZToggleBtn ? window.renderPGZToggleBtn() : '';
return `<div class="card"><div class="card-h"><div class="card-t">🏅 Savezi PGŽ — top 30 (od ${d.count||246})${onPGZ?' · ⭐ samo PGŽ-relevantni':''}</div></div>
${tb}
<table><thead><tr><th>Naziv</th><th class="num">Klubovi</th><th class="num">Sportaši</th><th>Predsjednik</th><th></th></tr></thead><tbody>${rows||'<tr><td colspan=5 class="empty">Učitavam...</td></tr>'}</tbody></table>
</div>`;
};
SECTIONS['pgz:klubovi'] = async () => {
const d = await api('/klubovi?limit=40') || {rows:[]};
const rows = (d.rows||[]).slice(0,40).map(k => `
<tr style="cursor:pointer" onclick="showDetail('klub',${k.id},${JSON.stringify(k.naziv)})">
<td><b>${esc(k.naziv)}</b></td>
const onPGZ = !!window._pgz_filter_priority;
const url = onPGZ ? '/klubovi?kategorija=priority&limit=80' : '/klubovi?limit=40';
const d = await api(url) || {rows:[]};
const bp = window.pgzBadgePrefix || (()=> '');
const rows = (d.rows||[]).slice(0,80).map(k => `
<tr style="cursor:pointer" onclick="showDetail('klub',${k.id},${JSON.stringify(k.naziv||k.klub)})">
<td><b>${bp(k)}${esc(k.naziv||k.klub||'—')}</b></td>
<td>${esc(k.savez||'—')}</td>
<td>${esc(k.grad||'—')}</td>
<td class="num">${fmt(k.broj_clanova||'—')}</td>
<td>${esc(k.predsjednik||'—')}</td>
</tr>`).join('');
return `<div class="card"><div class="card-h"><div class="card-t">⬢ Klubovi (${d.count||0})</div></div>
const tb = window.renderPGZToggleBtn ? window.renderPGZToggleBtn() : '';
return `<div class="card"><div class="card-h"><div class="card-t">⬢ Klubovi (${d.count||0})${onPGZ?' · ⭐ samo PGŽ-prioritet':''}</div></div>
${tb}
<table><thead><tr><th>Naziv</th><th>Savez</th><th>Grad</th><th class="num">Članova</th><th>Predsjednik</th></tr></thead><tbody>${rows||'<tr><td colspan=5 class="empty">—</td></tr>'}</tbody></table>
</div>`;
};
SECTIONS['pgz:sportasi'] = async () => {
const d = await api('/clanovi?limit=40') || {rows:[]};
const rows = (d.rows||[]).slice(0,40).map(c => `
const onPGZ = !!window._pgz_filter_priority;
const url = onPGZ ? '/v2/clanovi/priority-sort?only=true&limit=400' : '/clanovi?limit=40';
const d = await api(url) || {rows:[]};
const bp = window.pgzBadgePrefix || (()=> '');
const rows = (d.rows||[]).slice(0,80).map(c => `
<tr>
<td><b>${esc(c.ime+' '+(c.prezime||''))}</b></td>
<td>${esc(c.klub||'—')}</td>
<td><b>${bp(c)}${esc((c.ime||'')+' '+(c.prezime||''))}</b></td>
<td>${esc(c.klub||c.klub_naziv||c.klub_naziv_godisnjak||'—')}</td>
<td>${esc(c.kategorija||'—')}</td>
<td>${esc(c.spol||'—')}</td>
<td>${esc(c.datum_rodjenja||'—')}</td>
<td>${esc(c.datum_rodjenja||c.datum_rodenja||'—')}</td>
</tr>`).join('');
return `<div class="card"><div class="card-h"><div class="card-t">👤 Sportaši (${d.count||0})</div></div>
const tb = window.renderPGZToggleBtn ? window.renderPGZToggleBtn() : '';
return `<div class="card"><div class="card-h"><div class="card-t">👤 Sportaši (${d.count||0})${onPGZ?' · ⭐ samo PGŽ-prioritet':''}</div></div>
${tb}
<table><thead><tr><th>Ime i prezime</th><th>Klub</th><th>Kategorija</th><th>Spol</th><th>Rođen</th></tr></thead><tbody>${rows||'<tr><td colspan=5 class="empty">—</td></tr>'}</tbody></table>
</div>`;
};
@@ -1480,6 +1495,51 @@ SECTIONS['pgz:forenzika'] = () => `
</div>`).join('')}
</div>`;
// ─── pgz:dokumenti — knjižnica godišnjaka (in-page) ────────────────────
// Prikazuje grid kartica za 18+ godišnjaka iz pgz_sport.dokumenti.
// Klik na karticu → otvara PDF u novom tabu preko /api/v2/dokumenti/godisnjak/{godina}.
SECTIONS['pgz:dokumenti'] = async () => {
// Cache godisnjaci na _state da ne dohvaćamo svaki put
if(!_state._godisnjaci){
try {
const r = await fetch('/sport/api/v2/dokumenti/godisnjaci/list');
const j = await r.json();
_state._godisnjaci = (j && j.godisnjaci) || [];
} catch(e) { _state._godisnjaci = []; }
}
const docs = _state._godisnjaci.slice().sort((a,b)=> (b.godina||9999) - (a.godina||9999));
const fmtMB = b => b ? (b/1024/1024).toFixed(1)+' MB' : '—';
const cards = docs.map(d => {
const yr = d.godina || (d.izdano_datum ? String(d.izdano_datum).slice(0,4) : '—');
const url = d.godina ? `/sport/api/v2/dokumenti/godisnjak/${d.godina}` : `/sport/api/v2/dokumenti/${d.id}/pdf`;
return `
<div class="card" style="cursor:pointer;transition:transform 0.15s, border-color 0.15s"
onmouseover="this.style.borderColor='var(--gold,#F4C430)';this.style.transform='translateY(-2px)'"
onmouseout="this.style.borderColor='';this.style.transform=''"
onclick="window.open('${url}','_blank','noopener')">
<div style="font-size:1.9rem;font-weight:700;color:var(--gold,#F4C430);line-height:1;margin-bottom:6px;letter-spacing:-1px">${esc(yr)}</div>
<div style="font-weight:600;font-size:0.92rem;margin-bottom:6px">${esc(d.title || '(bez naslova)')}</div>
<div style="color:var(--t2);font-size:11px;margin-bottom:4px">🏛️ ${esc(d.organizacija || '—')}</div>
<div style="color:var(--t4);font-size:11px">📄 ${fmtMB(d.sadrzaj_size)}</div>
</div>`;
}).join('');
return `
<div class="card">
<div class="card-h">
<div class="card-t">📚 Dokumenti — Godišnjaci ZSP PGŽ</div>
<div class="card-actions">
<a href="/sport/dokumenti" class="btn primary" style="text-decoration:none">📖 Otvori knjižnicu</a>
</div>
</div>
<div style="color:var(--t2);font-size:12px;margin-bottom:14px">
${docs.length} godišnjaka u bazi · klik na karticu otvara PDF u novom tabu
</div>
${docs.length === 0
? '<div class="empty">Nema godišnjaka u bazi.</div>'
: `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px">${cards}</div>`}
</div>`;
};
// =======================================================================
// SAVEZ ADMIN — Dashboard + sub-pages
// =======================================================================
@@ -2095,12 +2155,31 @@ function navItemClick(item){
if(item && item.id) navTo(item.id);
}
// PGŽ priority filter helpers (CRISIS V4)
// PGŽ priority filter helpers (CRISIS V4 / SUB6)
window._pgz_filter_priority = window._pgz_filter_priority || false;
window.togglePGZFilter = function(){
window._pgz_filter_priority = !window._pgz_filter_priority;
if(typeof loadSection === 'function') loadSection();
};
window.pgzBadgePrefix = function(it){
const fin = !!(it && (it.financiran || it.klub_financiran || it.pgz_sufinanciran));
const god = !!(it && (it.godisnjak || it.klub_godisnjak || (it.godisnjak_godine && (it.godisnjak_godine.length||0)>0)));
const pgzs = !!(it && it.pgz_relevant);
const pri = !!(it && it.priority) || fin || god || pgzs;
if(!pri) return '';
let s = '⭐';
if(fin || pgzs) s += '💰';
if(god) s += '📖';
return s + ' ';
};
window.renderPGZToggleBtn = function(){
const on = !!window._pgz_filter_priority;
return '<button class="btn '+(on?'primary':'')+'" '
+ 'style="margin:6px 8px 10px 0" '
+ 'title="Prikaži samo PGŽ-financirane / u godišnjaku" '
+ 'onclick="togglePGZFilter()">'
+ (on ? '⭐ PGŽ filter ON' : '☆ PGŽ filter OFF') + '</button>';
};
</script>
</body>
</html>