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
+228 -12
View File
@@ -117,6 +117,9 @@ table tbody tr:hover{background:var(--bg3)}
<button class="tab" data-panel="glavna">📊 Glavna knjiga</button>
<button class="tab" data-panel="partneri">🤝 Partneri</button>
<button class="tab" data-panel="racuni">🧾 Računi</button>
<button class="tab" data-panel="uploads">📎 Uploads (OCR)</button>
<button class="tab" data-panel="putni">✈ Putni nalozi</button>
<button class="tab" data-panel="payments">💰 Plaćanja</button>
<button class="tab" data-panel="pdv">% PDV</button>
<button class="tab" data-panel="place">💼 Plaće</button>
<button class="tab" data-panel="proracun">€ Proračun</button>
@@ -198,7 +201,76 @@ table tbody tr:hover{background:var(--bg3)}
<button class="btn" onclick="loadRacuni()">Osvježi</button>
</div>
<div class="tbl-wrap">
<table id="rac-tbl"><thead><tr><th>#</th><th>Broj</th><th>Datum</th><th>Partner</th><th>OIB</th><th class="num">Neto</th><th class="num">PDV</th><th class="num">Brutto</th><th>Status</th><th>Akcije</th></tr></thead><tbody></tbody></table>
<table id="rac-tbl"><thead><tr><th>#</th><th>Broj</th><th>Datum</th><th>Partner</th><th>OIB</th><th class="num">Neto</th><th class="num">PDV</th><th class="num">Brutto</th><th>Status</th><th>Akcije</th></tr></thead><tbody><tr><td colspan="10" style="color:var(--t2);text-align:center;padding:18px">Klikni "Osvježi" za učitavanje…</td></tr></tbody></table>
</div>
<div id="rac-detail" style="display:none;margin-top:14px;border-top:1px solid var(--bd);padding-top:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<h4 id="rac-detail-title" style="font-size:12px;color:var(--t2);margin:0">Stavke</h4>
<button class="btn sec" onclick="document.getElementById('rac-detail').style.display='none'">× Zatvori</button>
</div>
<div class="tbl-wrap">
<table id="rac-stavke-tbl"><thead><tr><th>#</th><th>Naziv</th><th class="num">Količina</th><th>JM</th><th class="num">Cijena</th><th class="num">Popust %</th><th class="num">PDV %</th><th class="num">Neto</th><th class="num">Brutto</th></tr></thead><tbody></tbody></table>
</div>
<h4 id="rac-uploads-title" style="font-size:12px;color:var(--t2);margin:12px 0 6px;display:none">Privitci (uploads)</h4>
<div id="rac-uploads-wrap" style="display:none" class="tbl-wrap">
<table id="rac-uploads-tbl"><thead><tr><th>#</th><th>Datoteka</th><th class="num">Veličina</th><th>Mime</th><th>OCR</th><th>Datum</th></tr></thead><tbody></tbody></table>
</div>
</div>
</div>
</section>
<!-- ============ INVOICE UPLOADS (OCR) ============ -->
<section class="panel" id="panel-uploads">
<div class="card">
<div class="card-h"><div class="card-t">Invoice Uploads (OCR/AI extraction)</div></div>
<div class="toolbar">
<input id="up-q" placeholder="Datoteka / vendor / broj…">
<label>Status <select id="up-status"><option value="">— svi —</option><option value="pending">pending</option><option value="ocr_done">ocr_done</option><option value="approved">approved</option><option value="rejected">rejected</option></select></label>
<button class="btn" onclick="loadUploads()">Osvježi</button>
</div>
<div class="tbl-wrap">
<table id="up-tbl"><thead><tr><th>#</th><th>Datoteka</th><th class="num">Veličina</th><th>Vendor</th><th>OIB</th><th>Br. računa</th><th>Datum</th><th class="num">Brutto</th><th>OCR status</th><th class="num">Conf</th><th>Račun</th></tr></thead><tbody><tr><td colspan="11" style="color:var(--t2);text-align:center;padding:18px">Klikni "Osvježi"…</td></tr></tbody></table>
</div>
</div>
</section>
<!-- ============ PUTNI NALOZI / EXPENSE REPORTS ============ -->
<section class="panel" id="panel-putni">
<div class="card">
<div class="card-h"><div class="card-t">Putni nalozi i ostali troškovi (expense_reports)</div></div>
<div class="toolbar">
<label>Tip <select id="pn-type"><option value="">— svi —</option><option value="putni_nalog">Putni nalog</option><option value="expense">Trošak</option></select></label>
<label>Status <select id="pn-status"><option value="">— svi —</option><option value="draft">draft</option><option value="podnesen">podnesen</option><option value="odobren">odobren</option><option value="isplacen">isplacen</option><option value="rejected">rejected</option></select></label>
<label>Godina <input type="number" id="pn-godina" placeholder="2026" style="width:90px"></label>
<button class="btn" onclick="loadExpenseReports()">Osvježi</button>
</div>
<div class="tbl-wrap">
<table id="pn-tbl"><thead><tr><th>#</th><th>Tip</th><th>Klub</th><th>Odredište</th><th>Svrha</th><th>Od</th><th>Do</th><th class="num">Km</th><th class="num">Trošak</th><th class="num">Dnevnice</th><th>Status</th></tr></thead><tbody><tr><td colspan="11" style="color:var(--t2);text-align:center;padding:18px">Klikni "Osvježi"…</td></tr></tbody></table>
</div>
<div id="pn-detail" style="display:none;margin-top:14px;border-top:1px solid var(--bd);padding-top:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<h4 id="pn-detail-title" style="font-size:12px;color:var(--t2);margin:0">Vezani računi</h4>
<button class="btn sec" onclick="document.getElementById('pn-detail').style.display='none'">× Zatvori</button>
</div>
<div class="tbl-wrap">
<table id="pn-rac-tbl"><thead><tr><th>#</th><th>Broj računa</th><th>Vendor</th><th class="num">Brutto</th><th>Valuta</th><th>Kategorija</th><th>Datum</th></tr></thead><tbody></tbody></table>
</div>
</div>
</div>
</section>
<!-- ============ PAYMENTS ============ -->
<section class="panel" id="panel-payments">
<div class="card">
<div class="card-h"><div class="card-t">Plaćanja / Bank Reconciliation (payments)</div></div>
<div class="toolbar">
<label>Status <select id="py-status"><option value="">— svi —</option><option value="unmatched">unmatched</option><option value="matched">matched</option><option value="manual">manual</option></select></label>
<label>Način <select id="py-method"><option value="">— svi —</option><option value="transfer">transfer</option><option value="cash">cash</option><option value="card">card</option></select></label>
<label>Godina <input type="number" id="py-godina" placeholder="2026" style="width:90px"></label>
<button class="btn" onclick="loadPayments()">Osvježi</button>
</div>
<div class="tbl-wrap">
<table id="py-tbl"><thead><tr><th>#</th><th>Datum</th><th>Klub</th><th class="num">Iznos</th><th>Valuta</th><th>Način</th><th>IBAN OD</th><th>IBAN ZA</th><th>Referenca</th><th>Račun</th><th>Putni nalog</th><th>Match</th></tr></thead><tbody><tr><td colspan="12" style="color:var(--t2);text-align:center;padding:18px">Klikni "Osvježi"…</td></tr></tbody></table>
</div>
</div>
</section>
@@ -689,10 +761,33 @@ async function knjizi(tip, id){
catch(e){ alert(e.message); }
}
async function racDetail(tip, id){
const d = await api('/racuni/'+tip+'/'+id);
let html = `Račun ${d.head.broj||''} · ${d.head.partner_naziv}\nNeto: ${fmt(d.head.iznos_neto)} · PDV: ${fmt(d.head.iznos_pdv)} · Brutto: ${fmt(d.head.iznos_brutto)}\n\nSTAVKE:\n`;
d.stavke.forEach(s => html += `${s.naziv} ${s.kolicina}×${fmt(s.cijena_jed)} = ${fmt(s.iznos_brutto)}\n`);
alert(html);
try {
const d = await api('/racuni/'+tip+'/'+id);
document.getElementById('rac-detail').style.display = 'block';
document.getElementById('rac-detail-title').textContent =
`Stavke računa ${tip} · #${id} · ${d.head.broj||'(bez broja)'} · ${d.head.partner_naziv||''} · Neto ${fmt(d.head.iznos_neto)} · Brutto ${fmt(d.head.iznos_brutto)}`;
const sb = document.querySelector('#rac-stavke-tbl tbody');
sb.innerHTML = (d.stavke||[]).length
? d.stavke.map((s,ix)=>`<tr><td>${ix+1}</td><td>${s.naziv||''}</td><td class="num">${fmt(s.kolicina)}</td><td>${s.jed_mjera||''}</td><td class="num">${fmt(s.cijena_jed)}</td><td class="num">${fmt(s.popust_pct)}</td><td class="num">${fmt(s.pdv_pct)}</td><td class="num">${fmt(s.iznos_neto)}</td><td class="num"><b>${fmt(s.iznos_brutto)}</b></td></tr>`).join('')
: `<tr><td colspan="9" style="color:var(--t2);text-align:center;padding:14px">Nema stavki.</td></tr>`;
// For ulazni: show linked invoice_uploads (file_name)
const upT = document.getElementById('rac-uploads-title');
const upW = document.getElementById('rac-uploads-wrap');
if (tip === 'ulazni') {
upT.style.display='block'; upW.style.display='block';
try {
const u = await api('/racuni/ulazni/'+id+'/uploads');
const ub = document.querySelector('#rac-uploads-tbl tbody');
ub.innerHTML = (u.rows||[]).length
? u.rows.map(r=>`<tr><td>${r.id}</td><td><a href="/uploads/${r.file_path||''}" target="_blank">${r.file_name||''}</a></td><td class="num">${fmt((r.file_size||0)/1024)} KB</td><td>${r.mime||''}</td><td>${r.ocr_status||''}</td><td>${(r.uploaded_at||'').slice(0,10)}</td></tr>`).join('')
: `<tr><td colspan="6" style="color:var(--t2);text-align:center;padding:10px">Nema privitaka.</td></tr>`;
} catch(e){
document.querySelector('#rac-uploads-tbl tbody').innerHTML = `<tr><td colspan="6" style="color:var(--red)">Greška: ${e.message}</td></tr>`;
}
} else {
upT.style.display='none'; upW.style.display='none';
}
} catch(e) { alert('Greška: '+e.message); }
}
async function openRacunModal(tip){
await loadPartnerCache();
@@ -858,13 +953,131 @@ async function savePlaca(){
// ===== PRORAČUN =====
async function loadProracun(){
const d = await api('/proracun');
document.querySelector('#pr-tbl tbody').innerHTML = d.rows.map(r=>`<tr>
<td><b>${r.godina}</b></td><td class="num">${fmt(r.proracun_pgz)}</td>
<td class="num">${fmt(r.rebalans1)}</td><td class="num">${fmt(r.rebalans2)}</td>
<td class="num"><b>${fmt(r.ukupno_pgz)}</b></td><td class="num">${fmt(r.ministarstvo)}</td>
<td class="num"><b>${fmt(r.ukupno)}</b></td><td>${r.napomena||''}</td>
</tr>`).join('');
const tbody = document.querySelector('#pr-tbl tbody');
tbody.innerHTML = `<tr><td colspan="8" style="color:var(--t2);text-align:center;padding:14px">Učitavam…</td></tr>`;
try {
const d = await api('/proracun');
tbody.innerHTML = (d.rows||[]).length
? d.rows.map(r=>`<tr>
<td><b>${r.godina}</b></td><td class="num">${fmt(r.proracun_pgz)}</td>
<td class="num">${fmt(r.rebalans1)}</td><td class="num">${fmt(r.rebalans2)}</td>
<td class="num"><b>${fmt(r.ukupno_pgz)}</b></td><td class="num">${fmt(r.ministarstvo)}</td>
<td class="num"><b>${fmt(r.ukupno)}</b></td><td>${r.napomena||''}</td>
</tr>`).join('')
: `<tr><td colspan="8" style="color:var(--t2);text-align:center;padding:14px">Nema podataka.</td></tr>`;
} catch(e) {
tbody.innerHTML = `<tr><td colspan="8" style="color:var(--red)">Greška: ${e.message}</td></tr>`;
}
}
// ===== INVOICE UPLOADS =====
async function loadUploads(){
const tbody = document.querySelector('#up-tbl tbody');
tbody.innerHTML = `<tr><td colspan="11" style="color:var(--t2);text-align:center;padding:14px">Učitavam…</td></tr>`;
try {
const q = (document.getElementById('up-q').value||'').trim();
const st = document.getElementById('up-status').value;
const p = new URLSearchParams();
if(q) p.set('q', q);
if(st) p.set('ocr_status', st);
const d = await api('/invoice-uploads?'+p.toString());
tbody.innerHTML = (d.rows||[]).length
? d.rows.map(r=>`<tr>
<td>${r.id}</td>
<td><a href="/uploads/${r.file_path||''}" target="_blank">${r.file_name||''}</a></td>
<td class="num">${fmt((r.file_size||0)/1024)} KB</td>
<td>${r.ai_vendor_name||''}</td>
<td>${r.ai_vendor_oib||''}</td>
<td>${r.ai_invoice_no||''}</td>
<td>${r.ai_invoice_date||''}</td>
<td class="num">${fmt(r.ai_amount_gross)}</td>
<td><span class="badge ${r.ocr_status||''}">${r.ocr_status||'—'}</span></td>
<td class="num">${r.ocr_confidence!=null?fmt(r.ocr_confidence)+' %':''}</td>
<td>${r.invoice_id?('#'+r.invoice_id):'—'}</td>
</tr>`).join('')
: `<tr><td colspan="11" style="color:var(--t2);text-align:center;padding:14px">Nema uploadova.</td></tr>`;
} catch(e) {
tbody.innerHTML = `<tr><td colspan="11" style="color:var(--red)">Greška: ${e.message}</td></tr>`;
}
}
// ===== PUTNI NALOZI / EXPENSE REPORTS =====
async function loadExpenseReports(){
const tbody = document.querySelector('#pn-tbl tbody');
tbody.innerHTML = `<tr><td colspan="11" style="color:var(--t2);text-align:center;padding:14px">Učitavam…</td></tr>`;
try {
const t = document.getElementById('pn-type').value;
const s = document.getElementById('pn-status').value;
const g = document.getElementById('pn-godina').value;
const p = new URLSearchParams();
if(t) p.set('report_type', t);
if(s) p.set('status', s);
if(g) p.set('godina', g);
const d = await api('/expense-reports?'+p.toString());
tbody.innerHTML = (d.rows||[]).length
? d.rows.map(r=>`<tr onclick="expenseDetail(${r.id})" style="cursor:pointer">
<td>${r.id}</td>
<td>${r.report_type||''}</td>
<td>${r.klub_naziv||r.klub_id||''}</td>
<td>${r.destination||''}</td>
<td>${r.purpose||''}</td>
<td>${r.date_from||''}</td>
<td>${r.date_to||''}</td>
<td class="num">${fmt(r.km_driven)}</td>
<td class="num">${fmt(r.cost_total)}</td>
<td class="num">${fmt(r.dnevnice_amount)}</td>
<td><span class="badge ${r.status||''}">${r.status||''}</span></td>
</tr>`).join('')
: `<tr><td colspan="11" style="color:var(--t2);text-align:center;padding:14px">Nema putnih naloga.</td></tr>`;
} catch(e) {
tbody.innerHTML = `<tr><td colspan="11" style="color:var(--red)">Greška: ${e.message}</td></tr>`;
}
}
async function expenseDetail(id){
try {
document.getElementById('pn-detail').style.display='block';
document.getElementById('pn-detail-title').textContent = `Vezani računi za putni nalog #${id}`;
const d = await api('/putni-nalog-racuni?putni_nalog_id='+id);
const tb = document.querySelector('#pn-rac-tbl tbody');
tb.innerHTML = (d.rows||[]).length
? d.rows.map(r=>`<tr><td>${r.id}</td><td>${r.invoice_no||('#'+r.invoice_id)}</td><td>${r.vendor_name||''}</td><td class="num">${fmt(r.amount_gross)}</td><td>${r.currency||''}</td><td>${r.kategorija||''}</td><td>${(r.attached_at||'').slice(0,10)}</td></tr>`).join('')
: `<tr><td colspan="7" style="color:var(--t2);text-align:center;padding:10px">Nema vezanih računa.</td></tr>`;
} catch(e) { alert('Greška: '+e.message); }
}
// ===== PAYMENTS =====
async function loadPayments(){
const tbody = document.querySelector('#py-tbl tbody');
tbody.innerHTML = `<tr><td colspan="12" style="color:var(--t2);text-align:center;padding:14px">Učitavam…</td></tr>`;
try {
const s = document.getElementById('py-status').value;
const m = document.getElementById('py-method').value;
const g = document.getElementById('py-godina').value;
const p = new URLSearchParams();
if(s) p.set('matched_status', s);
if(m) p.set('payment_method', m);
if(g) p.set('godina', g);
const d = await api('/payments?'+p.toString());
tbody.innerHTML = (d.rows||[]).length
? d.rows.map(r=>`<tr>
<td>${r.id}</td>
<td>${r.payment_date||''}</td>
<td>${r.klub_naziv||r.klub_id||''}</td>
<td class="num"><b>${fmt(r.amount)}</b></td>
<td>${r.currency||''}</td>
<td>${r.payment_method||''}</td>
<td>${r.iban_from||''}</td>
<td>${r.iban_to||''}</td>
<td>${r.reference||''}</td>
<td>${r.invoice_id?('#'+r.invoice_id):'—'}</td>
<td>${r.expense_report_id?('#'+r.expense_report_id):'—'}</td>
<td><span class="badge ${r.matched_status||''}">${r.matched_status||''}</span></td>
</tr>`).join('')
: `<tr><td colspan="12" style="color:var(--t2);text-align:center;padding:14px">Nema plaćanja.</td></tr>`;
} catch(e) {
tbody.innerHTML = `<tr><td colspan="12" style="color:var(--red)">Greška: ${e.message}</td></tr>`;
}
}
// ===== IZVJEŠTAJI =====
@@ -925,6 +1138,9 @@ const loaders = {
glavna: loadGlavnaKnjiga,
partneri: loadPartneri,
racuni: loadRacuni,
uploads: loadUploads,
putni: loadExpenseReports,
payments: loadPayments,
pdv: loadPdv,
place: () => { loadZap(); loadPlace(); },
proracun: loadProracun,