From 16b980e84290ada08aa1e23601b8364f794f7333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Raduli=C4=87?= Date: Tue, 5 May 2026 13:17:56 +0200 Subject: [PATCH] =?UTF-8?q?6-sub=20sprint:=20Dokumenti+HNS=20profil+Admin+?= =?UTF-8?q?ERP+CRM+PG=C5=BD=20filter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- pgz_sport_v2_router.py | 50 ++++++++ routers/erp_full_router.py | 132 +++++++++++++++++++- static/app.html | 109 ++++++++++++++--- static/erp_full.html | 240 +++++++++++++++++++++++++++++++++++-- static/shared/sidebar.js | 14 ++- static/sport2.html | 157 +++++++++++++++++------- 6 files changed, 625 insertions(+), 77 deletions(-) diff --git a/pgz_sport_v2_router.py b/pgz_sport_v2_router.py index eb65790..2f4bd1f 100644 --- a/pgz_sport_v2_router.py +++ b/pgz_sport_v2_router.py @@ -5708,3 +5708,53 @@ def v2_sportasi_by_kategorija( out = sorted(groups.values(), key=lambda x: x["kategorija"]) return {"ok": True, "groups": out, "total_kategorija": len(out)} + +# ────────────────────────────────────────────────────────────────── +# PGŽ-financed (priority) thin wrappers for savezi & clanovi (SUB6) +# ────────────────────────────────────────────────────────────────── +@router.get("/savezi/priority-sort") +def v2_savezi_priority_sort(only: bool = False, limit: int = 500): + """Savezi sa pgz_relevant=true prvi (ili samo oni ako only=true).""" + where = "WHERE COALESCE(s.aktivan,true)" + if only: + where += " AND COALESCE(s.pgz_relevant,false) = TRUE" + rows = db_query(f""" + SELECT s.*, + COALESCE(s.pgz_relevant,false) AS priority, + (SELECT COUNT(*) FROM pgz_sport.klubovi WHERE savez_id=s.id) AS broj_klubova + FROM pgz_sport.savezi s + {where} + ORDER BY COALESCE(s.pgz_relevant,false) DESC, s.naziv COLLATE "hr-HR-x-icu" + LIMIT %s + """, (limit,)) + return {"count": len(rows), "rows": rows} + + +@router.get("/clanovi/priority-sort") +def v2_clanovi_priority_sort(only: bool = False, limit: int = 500): + """Sportaši čiji klub je PGŽ-financiran ili u godišnjaku — prioritetni prvi.""" + priority_expr = ("(COALESCE(k.pgz_sufinanciran,false) " + "OR (k.godisnjak_godine IS NOT NULL " + "AND array_length(k.godisnjak_godine,1) > 0))") + where = "WHERE c.aktivan = TRUE" + if only: + where += f" AND {priority_expr}" + rows = db_query(f""" + SELECT c.id, c.ime, c.prezime, c.oib, c.datum_rodenja, c.spol, c.sport, + c.pozicija, c.reprezentativac, c.kategoriziran, c.stipendiran, + c.kategorija, c.kategorije, c.kategorija_hoo, c.hoo_kategorija, + c.aktivan, c.klub_id, c.klub_naziv_godisnjak, c.slika_url, + c.broj_dresa, c.uloga, + k.naziv AS klub_naziv, + COALESCE(k.pgz_sufinanciran,false) AS klub_financiran, + (k.godisnjak_godine IS NOT NULL + AND array_length(k.godisnjak_godine,1) > 0) AS klub_godisnjak, + {priority_expr} AS priority + FROM pgz_sport.clanovi c + LEFT JOIN pgz_sport.klubovi k ON k.id = c.klub_id + {where} + ORDER BY {priority_expr} DESC, c.prezime, c.ime + LIMIT %s + """, (limit,)) + return {"count": len(rows), "rows": rows} + diff --git a/routers/erp_full_router.py b/routers/erp_full_router.py index f82b3cd..d664443 100644 --- a/routers/erp_full_router.py +++ b/routers/erp_full_router.py @@ -1134,14 +1134,142 @@ def proracun_list(): # ═══════════════════════════════════════════════════════════════════ -# 11) HEALTH/DEBUG +# 11) INVOICE UPLOADS (PDF/scan attachments to ulazni računi) +# ═══════════════════════════════════════════════════════════════════ +@router.get("/invoice-uploads") +def invoice_uploads_list(klub_id: Optional[int] = None, + ocr_status: Optional[str] = None, + q: Optional[str] = None, + limit: int = 200): + where = ["1=1"] + params: list = [] + if klub_id: + where.append("klub_id=%s"); params.append(klub_id) + if ocr_status: + where.append("ocr_status=%s"); params.append(ocr_status) + if q: + where.append("(file_name ILIKE %s OR ai_vendor_name ILIKE %s OR ai_invoice_no ILIKE %s)") + params.extend([f"%{q}%", f"%{q}%", f"%{q}%"]) + params.append(limit) + rows = db_query( + "SELECT id, klub_id, file_name, file_path, file_size, mime, ocr_status, " + "ocr_confidence, ai_invoice_no, ai_invoice_date, ai_vendor_name, ai_vendor_oib, " + "ai_amount_gross, ai_currency, invoice_id, uploaded_by, " + "uploaded_at FROM pgz_sport.invoice_uploads " + f"WHERE {' AND '.join(where)} ORDER BY id DESC LIMIT %s", + tuple(params)) + return {"count": len(rows), "rows": rows} + + +@router.get("/racuni/ulazni/{rid}/uploads") +def racuni_ulazni_uploads(rid: int): + """Uploads (file attachments) linked to an ulazni racun via invoice_id.""" + rows = db_query( + "SELECT id, file_name, file_path, file_size, mime, ocr_status, " + "ai_invoice_no, ai_vendor_name, ai_amount_gross, uploaded_at " + "FROM pgz_sport.invoice_uploads WHERE invoice_id=%s ORDER BY id DESC", + (rid,)) + return {"count": len(rows), "rows": rows} + + +# ═══════════════════════════════════════════════════════════════════ +# 12) PUTNI NALOZI / EXPENSE REPORTS +# ═══════════════════════════════════════════════════════════════════ +@router.get("/expense-reports") +def expense_reports_list(klub_id: Optional[int] = None, + status: Optional[str] = None, + report_type: Optional[str] = None, + godina: Optional[int] = None, + limit: int = 200): + where = ["1=1"] + params: list = [] + if klub_id: + where.append("er.klub_id=%s"); params.append(klub_id) + if status: + where.append("er.status=%s"); params.append(status) + if report_type: + where.append("er.report_type=%s"); params.append(report_type) + if godina: + where.append("EXTRACT(YEAR FROM er.date_from)=%s"); params.append(godina) + params.append(limit) + rows = db_query( + "SELECT er.id, er.klub_id, k.naziv AS klub_naziv, er.report_type, er.report_no, " + "er.destination, er.purpose, er.date_from, er.date_to, er.km_driven, " + "er.cost_total, er.dnevnice_count, er.dnevnice_amount, er.status, " + "er.approved_at, er.paid_at, er.created_at " + "FROM pgz_sport.expense_reports er " + "LEFT JOIN pgz_sport.klubovi k ON k.id=er.klub_id " + f"WHERE {' AND '.join(where)} ORDER BY er.id DESC LIMIT %s", + tuple(params)) + return {"count": len(rows), "rows": rows} + + +@router.get("/putni-nalog-racuni") +def putni_nalog_racuni_list(putni_nalog_id: Optional[int] = None, + invoice_id: Optional[int] = None, + limit: int = 200): + where = ["1=1"] + params: list = [] + if putni_nalog_id: + where.append("pnr.putni_nalog_id=%s"); params.append(putni_nalog_id) + if invoice_id: + where.append("pnr.invoice_id=%s"); params.append(invoice_id) + params.append(limit) + rows = db_query( + "SELECT pnr.id, pnr.putni_nalog_id, pnr.invoice_id, pnr.kategorija, " + "pnr.napomena, pnr.attached_at, " + "er.report_no, er.destination, er.purpose, " + "i.invoice_no, i.vendor_name, i.amount_gross, i.currency " + "FROM pgz_sport.putni_nalog_racuni pnr " + "LEFT JOIN pgz_sport.expense_reports er ON er.id=pnr.putni_nalog_id " + "LEFT JOIN pgz_sport.invoices i ON i.id=pnr.invoice_id " + f"WHERE {' AND '.join(where)} ORDER BY pnr.id DESC LIMIT %s", + tuple(params)) + return {"count": len(rows), "rows": rows} + + +# ═══════════════════════════════════════════════════════════════════ +# 13) PAYMENTS (uplate/isplate, bank reconciliation) +# ═══════════════════════════════════════════════════════════════════ +@router.get("/payments") +def payments_list(klub_id: Optional[int] = None, + matched_status: Optional[str] = None, + payment_method: Optional[str] = None, + godina: Optional[int] = None, + limit: int = 200): + where = ["1=1"] + params: list = [] + if klub_id: + where.append("p.klub_id=%s"); params.append(klub_id) + if matched_status: + where.append("p.matched_status=%s"); params.append(matched_status) + if payment_method: + where.append("p.payment_method=%s"); params.append(payment_method) + if godina: + where.append("EXTRACT(YEAR FROM p.payment_date)=%s"); params.append(godina) + params.append(limit) + rows = db_query( + "SELECT p.id, p.klub_id, k.naziv AS klub_naziv, p.invoice_id, " + "p.expense_report_id, p.clanarina_id, p.payment_date, p.amount, p.currency, " + "p.payment_method, p.iban_from, p.iban_to, p.reference, p.description, " + "p.bank_statement_no, p.bank_transaction_id, p.matched_status, p.created_at " + "FROM pgz_sport.payments p " + "LEFT JOIN pgz_sport.klubovi k ON k.id=p.klub_id " + f"WHERE {' AND '.join(where)} ORDER BY p.payment_date DESC, p.id DESC LIMIT %s", + tuple(params)) + return {"count": len(rows), "rows": rows} + + +# ═══════════════════════════════════════════════════════════════════ +# 14) HEALTH/DEBUG # ═══════════════════════════════════════════════════════════════════ @router.get("/health") def erp_health(): counts = {} for t in ["kontni_plan", "partneri", "dnevnik_zapisa", "knjizenja", "racuni_ulazni", "racuni_izlazni", "racun_stavke", - "zaposlenici", "place_obracun"]: + "zaposlenici", "place_obracun", "proracun", + "invoice_uploads", "expense_reports", "putni_nalog_racuni", "payments"]: try: r = db_one(f"SELECT COUNT(*) AS c FROM pgz_sport.{t}") counts[t] = r["c"] if r else 0 diff --git a/static/app.html b/static/app.html index 5e417bb..7345a65 100644 --- a/static/app.html +++ b/static/app.html @@ -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 => ` - ${esc(s.naziv)} + ${bp(s)}${esc(s.naziv)} ${fmt(s.broj_klubova||'—')} ${fmt(s.broj_sportasa||'—')} ${esc(s.predsjednik||'—')} `).join(''); - return `
🏅 Savezi PGŽ — top 30 (od ${d.count||246})
+ const tb = window.renderPGZToggleBtn ? window.renderPGZToggleBtn() : ''; + return `
🏅 Savezi PGŽ — top 30 (od ${d.count||246})${onPGZ?' · ⭐ samo PGŽ-relevantni':''}
+ ${tb} ${rows||''}
NazivKluboviSportašiPredsjednik
Učitavam...
`; }; SECTIONS['pgz:klubovi'] = async () => { - const d = await api('/klubovi?limit=40') || {rows:[]}; - const rows = (d.rows||[]).slice(0,40).map(k => ` - - ${esc(k.naziv)} + 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 => ` + + ${bp(k)}${esc(k.naziv||k.klub||'—')} ${esc(k.savez||'—')} ${esc(k.grad||'—')} ${fmt(k.broj_clanova||'—')} ${esc(k.predsjednik||'—')} `).join(''); - return `
⬢ Klubovi (${d.count||0})
+ const tb = window.renderPGZToggleBtn ? window.renderPGZToggleBtn() : ''; + return `
⬢ Klubovi (${d.count||0})${onPGZ?' · ⭐ samo PGŽ-prioritet':''}
+ ${tb} ${rows||''}
NazivSavezGradČlanovaPredsjednik
`; }; 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 => ` - ${esc(c.ime+' '+(c.prezime||''))} - ${esc(c.klub||'—')} + ${bp(c)}${esc((c.ime||'')+' '+(c.prezime||''))} + ${esc(c.klub||c.klub_naziv||c.klub_naziv_godisnjak||'—')} ${esc(c.kategorija||'—')} ${esc(c.spol||'—')} - ${esc(c.datum_rodjenja||'—')} + ${esc(c.datum_rodjenja||c.datum_rodenja||'—')} `).join(''); - return `
👤 Sportaši (${d.count||0})
+ const tb = window.renderPGZToggleBtn ? window.renderPGZToggleBtn() : ''; + return `
👤 Sportaši (${d.count||0})${onPGZ?' · ⭐ samo PGŽ-prioritet':''}
+ ${tb} ${rows||''}
Ime i prezimeKlubKategorijaSpolRođen
`; }; @@ -1480,6 +1495,51 @@ SECTIONS['pgz:forenzika'] = () => `
`).join('')}
`; +// ─── 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 ` +
+
${esc(yr)}
+
${esc(d.title || '(bez naslova)')}
+
🏛️ ${esc(d.organizacija || '—')}
+
📄 ${fmtMB(d.sadrzaj_size)}
+
`; + }).join(''); + return ` +
+
+
📚 Dokumenti — Godišnjaci ZSP PGŽ
+ +
+
+ ${docs.length} godišnjaka u bazi · klik na karticu otvara PDF u novom tabu +
+ ${docs.length === 0 + ? '
Nema godišnjaka u bazi.
' + : `
${cards}
`} +
`; +}; + // ======================================================================= // 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 ''; +}; diff --git a/static/erp_full.html b/static/erp_full.html index d9632d1..502c648 100644 --- a/static/erp_full.html +++ b/static/erp_full.html @@ -117,6 +117,9 @@ table tbody tr:hover{background:var(--bg3)} + + + @@ -198,7 +201,76 @@ table tbody tr:hover{background:var(--bg3)}
-
#BrojDatumPartnerOIBNetoPDVBruttoStatusAkcije
+
#BrojDatumPartnerOIBNetoPDVBruttoStatusAkcije
Klikni "Osvježi" za učitavanje…
+
+ + + + + +
+
+
Invoice Uploads (OCR/AI extraction)
+
+ + + +
+
+
#DatotekaVeličinaVendorOIBBr. računaDatumBruttoOCR statusConfRačun
Klikni "Osvježi"…
+
+
+
+ + +
+
+
Putni nalozi i ostali troškovi (expense_reports)
+
+ + + + +
+
+
#TipKlubOdredišteSvrhaOdDoKmTrošakDnevniceStatus
Klikni "Osvježi"…
+
+ +
+
+ + +
+
+
Plaćanja / Bank Reconciliation (payments)
+
+ + + + +
+
+
#DatumKlubIznosValutaNačinIBAN ODIBAN ZAReferencaRačunPutni nalogMatch
Klikni "Osvježi"…
@@ -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)=>`${ix+1}${s.naziv||''}${fmt(s.kolicina)}${s.jed_mjera||''}${fmt(s.cijena_jed)}${fmt(s.popust_pct)}${fmt(s.pdv_pct)}${fmt(s.iznos_neto)}${fmt(s.iznos_brutto)}`).join('') + : `Nema stavki.`; + // 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=>`${r.id}${r.file_name||''}${fmt((r.file_size||0)/1024)} KB${r.mime||''}${r.ocr_status||''}${(r.uploaded_at||'').slice(0,10)}`).join('') + : `Nema privitaka.`; + } catch(e){ + document.querySelector('#rac-uploads-tbl tbody').innerHTML = `Greška: ${e.message}`; + } + } 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=>` - ${r.godina}${fmt(r.proracun_pgz)} - ${fmt(r.rebalans1)}${fmt(r.rebalans2)} - ${fmt(r.ukupno_pgz)}${fmt(r.ministarstvo)} - ${fmt(r.ukupno)}${r.napomena||''} - `).join(''); + const tbody = document.querySelector('#pr-tbl tbody'); + tbody.innerHTML = `Učitavam…`; + try { + const d = await api('/proracun'); + tbody.innerHTML = (d.rows||[]).length + ? d.rows.map(r=>` + ${r.godina}${fmt(r.proracun_pgz)} + ${fmt(r.rebalans1)}${fmt(r.rebalans2)} + ${fmt(r.ukupno_pgz)}${fmt(r.ministarstvo)} + ${fmt(r.ukupno)}${r.napomena||''} + `).join('') + : `Nema podataka.`; + } catch(e) { + tbody.innerHTML = `Greška: ${e.message}`; + } +} + +// ===== INVOICE UPLOADS ===== +async function loadUploads(){ + const tbody = document.querySelector('#up-tbl tbody'); + tbody.innerHTML = `Učitavam…`; + 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=>` + ${r.id} + ${r.file_name||''} + ${fmt((r.file_size||0)/1024)} KB + ${r.ai_vendor_name||''} + ${r.ai_vendor_oib||''} + ${r.ai_invoice_no||''} + ${r.ai_invoice_date||''} + ${fmt(r.ai_amount_gross)} + ${r.ocr_status||'—'} + ${r.ocr_confidence!=null?fmt(r.ocr_confidence)+' %':''} + ${r.invoice_id?('#'+r.invoice_id):'—'} + `).join('') + : `Nema uploadova.`; + } catch(e) { + tbody.innerHTML = `Greška: ${e.message}`; + } +} + +// ===== PUTNI NALOZI / EXPENSE REPORTS ===== +async function loadExpenseReports(){ + const tbody = document.querySelector('#pn-tbl tbody'); + tbody.innerHTML = `Učitavam…`; + 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=>` + ${r.id} + ${r.report_type||''} + ${r.klub_naziv||r.klub_id||''} + ${r.destination||''} + ${r.purpose||''} + ${r.date_from||''} + ${r.date_to||''} + ${fmt(r.km_driven)} + ${fmt(r.cost_total)} + ${fmt(r.dnevnice_amount)} + ${r.status||''} + `).join('') + : `Nema putnih naloga.`; + } catch(e) { + tbody.innerHTML = `Greška: ${e.message}`; + } +} + +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=>`${r.id}${r.invoice_no||('#'+r.invoice_id)}${r.vendor_name||''}${fmt(r.amount_gross)}${r.currency||''}${r.kategorija||''}${(r.attached_at||'').slice(0,10)}`).join('') + : `Nema vezanih računa.`; + } catch(e) { alert('Greška: '+e.message); } +} + +// ===== PAYMENTS ===== +async function loadPayments(){ + const tbody = document.querySelector('#py-tbl tbody'); + tbody.innerHTML = `Učitavam…`; + 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=>` + ${r.id} + ${r.payment_date||''} + ${r.klub_naziv||r.klub_id||''} + ${fmt(r.amount)} + ${r.currency||''} + ${r.payment_method||''} + ${r.iban_from||''} + ${r.iban_to||''} + ${r.reference||''} + ${r.invoice_id?('#'+r.invoice_id):'—'} + ${r.expense_report_id?('#'+r.expense_report_id):'—'} + ${r.matched_status||''} + `).join('') + : `Nema plaćanja.`; + } catch(e) { + tbody.innerHTML = `Greška: ${e.message}`; + } } // ===== 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, diff --git a/static/shared/sidebar.js b/static/shared/sidebar.js index cb61420..4c521b0 100644 --- a/static/shared/sidebar.js +++ b/static/shared/sidebar.js @@ -35,12 +35,14 @@ {id:'notif', ic:'\u{1F514}', label:'Notifikacije', href:'/app#notif'} ]}, {title:'CRM', items: [ + {id:'crm_v2', ic:'\u{1F3AF}', label:'CRM (Salesforce-Lite)', href:'/sport/crm_v2'}, {id:'clanarine', ic:'\u{1F4B3}', label:'Članarine', href:'/crm#clanarine'}, {id:'lijecnicki',ic:'⚕', label:'Liječnički', href:'/crm#lijecnicki'}, {id:'obrasci', ic:'\u{1F4CB}', label:'Obrasci', href:'/crm#obrasci'}, {id:'dokumenti', ic:'\u{1F4C4}', label:'Dokumenti', href:'/crm#dokumenti'} ]}, {title:'ERP', items: [ + {id:'erpfull', ic:'\u{1F4D2}', label:'ERP Full (SAP-Lite)', href:'/erp/full'}, {id:'racuni', ic:'\u{1F9FE}', label:'Računi (OCR)', href:'/erp#racuni'}, {id:'putni', ic:'✈', label:'Putni nalozi', href:'/erp#putni'}, {id:'placanja', ic:'\u{1F4B0}', label:'Plaćanja', href:'/erp#placanja'}, @@ -54,12 +56,12 @@ {id:'audit', ic:'\u{1F512}', label:'Audit log', href:'/audit'} ]}, {title:'ADMIN', requireRole:['pgz_admin','super_admin'], items: [ - {id:'users', ic:'\u{1F465}', label:'Korisnici', href:'/admin/users#users'}, - {id:'tenants', ic:'\u{1F3E2}', label:'Tenanti', href:'/admin/users#tenants'}, - {id:'security', ic:'\u{1F6E1}', label:'Sigurnost', href:'/admin/users#security'}, - {id:'rbac', ic:'\u{1F511}', label:'RBAC matrica', href:'/admin/users#rbac'}, - {id:'audit', ic:'\u{1F512}', label:'Audit log', href:'/admin/users#audit'}, - {id:'gdpr', ic:'\u{1F512}', label:'GDPR', href:'/admin/users#gdpr'} + {id:'users', ic:'\u{1F465}', label:'Korisnici', href:'/sport/admin/users#users'}, + {id:'tenants', ic:'\u{1F3E2}', label:'Tenanti', href:'/sport/admin/users#tenants'}, + {id:'security', ic:'\u{1F6E1}', label:'Sigurnost', href:'/sport/admin/users#security'}, + {id:'rbac', ic:'\u{1F511}', label:'RBAC matrica', href:'/sport/admin/users#rbac'}, + {id:'auditlog', ic:'\u{1F512}', label:'Audit log', href:'/sport/admin/users#audit'}, + {id:'gdpr', ic:'\u{1F510}', label:'GDPR', href:'/sport/admin/users#gdpr'} ]} ]; diff --git a/static/sport2.html b/static/sport2.html index a3b9af1..4e34794 100644 --- a/static/sport2.html +++ b/static/sport2.html @@ -183,6 +183,21 @@ a.tag:hover,.tag[onclick]:hover{transform:translateY(-1px);filter:brightness(1.1 .pp-stat{text-align:center} .pp-stat .v{font-size:20px;font-weight:800;color:var(--pgz-gold);font-family:var(--mono)} .pp-stat .l{font-size:9px;color:var(--t4);text-transform:uppercase;letter-spacing:.5px;margin-top:2px;font-weight:600} +.pp-bio-row{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px;font-size:11px;color:var(--t2)} +.pp-bio-chip{padding:3px 8px;background:var(--bg3);border:1px solid var(--rim);border-radius:4px;color:var(--t1);font-weight:500} +.pp-bio-chip b{color:var(--pgz-gold);font-family:var(--mono);font-weight:700;margin-right:4px} +.pp-links{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px} +.pp-link{display:inline-flex;align-items:center;gap:6px;padding:6px 11px;background:var(--bg3);border:1px solid var(--rim2);border-radius:5px;color:var(--t1);font-size:11.5px;font-weight:600;text-decoration:none;cursor:pointer;transition:all .15s} +.pp-link:hover{background:var(--bg4);border-color:var(--pgz-gold);color:var(--pgz-gold);transform:translateY(-1px)} +.pp-link.hns{border-color:#0066cc;color:#3aa8ff} +.pp-link.hns:hover{background:rgba(0,102,204,.15);color:#5cc8ff;border-color:#3aa8ff} +.pp-link.gg{border-color:#9b8aff;color:#b9aeff} +.pp-link.gg:hover{background:rgba(155,138,255,.15);color:#cfc6ff} +.pp-link.wiki{border-color:#aaa;color:#ddd} +.pp-link.wiki:hover{background:rgba(255,255,255,.06);color:#fff} +.pp-section{margin-top:18px} +.pp-section-h{display:flex;align-items:center;gap:8px;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid var(--rim);font-size:13px;font-weight:700;color:var(--t0);letter-spacing:.3px} +.pp-section-h .cnt{font-size:10.5px;color:var(--t4);font-weight:600;font-family:var(--mono)} .kv{display:grid;grid-template-columns:140px 1fr;gap:6px 12px;font-size:12px} .kv .k{color:var(--t2);font-weight:600} @@ -343,6 +358,42 @@ const _state = {section:'dashboard', viewSavezi:'card', viewKlubovi:'card', view const _sort = {savezi:null, klubovi:null, sportasi:null, objekti:null, manifestacije:null, financije:null}; let _proracunChart=null, _financijeChart=null; +// === PGŽ priority filter (SUB6) — global helper, works across Klubovi/Savezi/Sportaši === +window._pgz_filter_priority = window._pgz_filter_priority || false; +window.togglePGZFilter = function(section){ + window._pgz_filter_priority = !window._pgz_filter_priority; + // Drop caches so the next load fetches priority-only or full set. + if(!section || section==='klubovi') _cache.klubovi = null; + if(!section || section==='savezi') _cache.savezi = null; + if(!section || section==='sportasi') _cache.clanovi = null; + // Reload whichever section is active. + const cur = (section || _state.section); + if(cur==='klubovi') loadKlubovi(); + else if(cur==='savezi') loadSavezi(); + else if(cur==='sportasi') loadSportasi(); + else loadSection(_state.section); +}; +window.pgzBadgePrefix = function(it, kind){ + // returns ⭐💰📖 prefix tailored to which markers apply + 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(section){ + const on = !!window._pgz_filter_priority; + return ''; +}; + + //=========== UTIL =========== const $ = s => document.querySelector(s); const $$ = s => Array.from(document.querySelectorAll(s)); @@ -1129,7 +1180,11 @@ async function loadSavezi(){ const root = $('#pg-savezi'); if(!_cache.savezi){ root.innerHTML = '
Učitavanje saveza…
'; - const d = await api('/savezi?limit=250'); + // PGŽ filter: switch to v2 priority-sort endpoint (only=true returns just PGŽ-relevant savezi) + const url = window._pgz_filter_priority + ? '/v2/savezi/priority-sort?only=true&limit=500' + : '/savezi?limit=250'; + const d = await api(url); if(!d){ root.innerHTML='
Greška pri dohvatu
'; return; } _cache.savezi = d.rows || []; } @@ -1156,6 +1211,7 @@ function renderSaveziShell(){ + ${window.renderPGZToggleBtn ? window.renderPGZToggleBtn('savezi') : ''}
@@ -1191,7 +1247,7 @@ function renderSaveziGrid(rows){ return '
'+rows.map(s => `
${s.pgz_relevant?'
PGŽ
':''} -
${esc(s.naziv)}
+
${(window.pgzBadgePrefix?window.pgzBadgePrefix(s,'savez'):'')}${esc(s.naziv)}
${txt(s.sport,'—')} · ${txt(s.predsjednik,'bez predsjednika')}
${fmtNum(s.broj_klubova)} klubova @@ -1206,7 +1262,7 @@ function renderSaveziTable(rows){ ${sortHeader('savezi','naziv','Naziv','')}${sortHeader('savezi','sport','Sport','')}${sortHeader('savezi','predsjednik','Predsjednik','')}${sortHeader('savezi','email','Email','')}${sortHeader('savezi','broj_klubova','Klubova','num')}${sortHeader('savezi','reg_2024','Reg.','num')}${sortHeader('savezi','pgz_relevant','PGŽ','')} ${rows.map(s => ` - ${esc(s.naziv)} + ${(window.pgzBadgePrefix?window.pgzBadgePrefix(s,'savez'):'')}${esc(s.naziv)} ${txt(s.sport)} ${txt(s.predsjednik)} ${s.email?''+esc(s.email)+'':'—'} @@ -1275,40 +1331,18 @@ async function openSavez(id){ } //=========== KLUBOVI =========== -async -// === PGŽ FINANCIRANI FILTER (CRISIS V4) === -window._klubFilters = window._klubFilters || {financiran: false, godisnjak: false}; +// (legacy _klubFilters helpers replaced by global togglePGZFilter — SUB6) -window.toggleKlubFilter = function(name){ - window._klubFilters[name] = !window._klubFilters[name]; - loadKlubovi(); -}; - -window.renderKlubFilters = function(targetEl){ - if(!targetEl) return; - const f = window._klubFilters; - targetEl.innerHTML = ` - - - - `; -}; - -function loadKlubovi(){ +async function loadKlubovi(){ const root = $('#pg-klubovi'); if(!_cache.klubovi){ root.innerHTML = '
Učitavanje klubova…
'; - // request all clubs sorted by priority (financed-or-godišnjak first) from backend - const d = await api('/klubovi?limit=2500'); + // /api/klubovi already returns priority/financiran/godisnjak flags. + // When PGŽ filter is on, ask backend to only return priority klubs. + const url = window._pgz_filter_priority + ? '/klubovi?kategorija=priority&limit=2500' + : '/klubovi?limit=2500'; + const d = await api(url); if(!d){ root.innerHTML='
Greška pri dohvatu
'; return; } _cache.klubovi = d.rows || []; } @@ -1338,6 +1372,7 @@ function renderKluboviShell(){ + ${window.renderPGZToggleBtn ? window.renderPGZToggleBtn('klubovi') : ''}
@@ -1388,7 +1423,7 @@ function renderKluboviGrid(rows){ return '
'+rows.map(k => `
${k.priority?'
★ PRIO
':(k.nositelj_kvalitete?'
N.K.
':'')} -
${esc(k.klub||k.sport||'(bez naziva)')}
+
${(window.pgzBadgePrefix?window.pgzBadgePrefix(k,'klub'):'')}${esc(k.klub||k.sport||'(bez naziva)')}
${txt(k.razina,'')} · ${txt(k.grad,'—')}
${k.financiran?'':''} @@ -1407,7 +1442,7 @@ function renderKluboviTable(rows){ ${k.priority?'':''} - ${esc(k.klub||k.sport||'(bez naziva)')} + ${(window.pgzBadgePrefix?window.pgzBadgePrefix(k,'klub'):'')}${esc(k.klub||k.sport||'(bez naziva)')} ${txt(k.sport)} ${txt(k.razina)} ${txt(k.grad)} @@ -1678,7 +1713,11 @@ async function loadSportasi(){ const root = $('#pg-sportasi'); if(!_cache.clanovi){ root.innerHTML = '
Učitavanje sportaša…
'; - const d = await api('/clanovi-full?limit=500'); + // PGŽ filter: switch to v2 priority-sort (joins club, marks priority) + const url = window._pgz_filter_priority + ? '/v2/clanovi/priority-sort?only=true&limit=2000' + : '/clanovi-full?limit=500'; + const d = await api(url); if(!d){ root.innerHTML='
Greška pri dohvatu
'; return; } _cache.clanovi = d.rows || []; } @@ -1718,6 +1757,7 @@ function renderSportasiShell(){
+ ${window.renderPGZToggleBtn ? window.renderPGZToggleBtn('sportasi') : ''} @@ -1855,7 +1895,7 @@ function buildPlayerCard(c){
${photo}
-
${esc(c.ime||'')} ${esc(c.prezime||'')}
+
${(window.pgzBadgePrefix?window.pgzBadgePrefix(c,'sportas'):'')}${esc(c.ime||'')} ${esc(c.prezime||'')}
${txt(c.sport,'—')} · ${txt(c.pozicija,'')}
${txt(c.klub_naziv_godisnjak,'')}
@@ -1873,7 +1913,7 @@ function renderSportasiTable(rows){ ${sortHeader('sportasi','prezime','Prezime','')}${sortHeader('sportasi','ime','Ime','')}${sortHeader('sportasi','sport','Sport','')}${sortHeader('sportasi','pozicija','Pozicija','')}${sortHeader('sportasi','hoo_kategorija','HOO','')}${sortHeader('sportasi','reprezentativac','Repr.','')}${sortHeader('sportasi','slika_url','Foto','')} ${rows.map(c => ` - ${esc(c.prezime||'')} + ${(window.pgzBadgePrefix?window.pgzBadgePrefix(c,'sportas'):'')}${esc(c.prezime||'')} ${esc(c.ime||'')} ${txt(c.sport)} ${txt(c.pozicija)} @@ -1886,11 +1926,23 @@ function renderSportasiTable(rows){ async function openSportas(id){ openPanel('Sportaš', '
Učitavanje profila…
'); - const d = await api('/sportas/'+id+'/profil'); - if(!d || d.detail){ + // Pull primary profile + (optionally) richer v2 dossier in parallel. + // /sportas/{id}/profil = sezone+utakmice (primary). /v2/clan/{id}/full = canonical multi-sport fields. + const [dRaw, dV2] = await Promise.all([ + api('/sportas/'+id+'/profil'), + api('/v2/clan/'+id+'/full').catch(()=>null) + ]); + if(!dRaw || dRaw.detail){ openPanel('Sportaš', '
Sportaš nije pronađen
'); return; } + // Merge: primary wins, but pull missing height/weight/foot/biografija from v2 dossier + const d = Object.assign({}, dRaw); + if(dV2 && dV2.profile){ + const p = dV2.profile; + ['visina_cm','tezina_kg','dominantna_noga','broj_dresa','biografija','mjesto_rodenja','mjesto_rodjenja','datum_rodenja','datum_rodjenja','spol','sport','aktivan'] + .forEach(k => { if((d[k]===null||d[k]===undefined||d[k]==='') && p[k]!==null && p[k]!==undefined && p[k]!=='') d[k]=p[k]; }); + } const stats = d.stats || {}; const sezone = d.clan_sezona || []; const utakmice = d.utakmice || []; @@ -1900,6 +1952,13 @@ async function openSportas(id){ const photo = d.slika_url ? '' : '
'+initials+'
'; const dob = d.datum_rodjenja || d.datum_rodenja; const hooCat = d.hoo_kategorija || d.kategorija_hoo; + // Build HNS deep link: prefer profile_url; otherwise compose /igraci/{hns_id}/{slug}/ from semafor when we have hns_igrac_id. + const slug = d.slug || ((d.ime||'')+'-'+(d.prezime||'')).toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g,'').replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,''); + const hnsId = d.hns_igrac_id || d.source_id || (d.vanjski_id && d.vanjski_id.hns_semafor) || null; + const hnsUrl = d.profile_url || (hnsId ? ('https://semafor.hns.family/igraci/'+encodeURIComponent(hnsId)+'/'+encodeURIComponent(slug)+'/') : null); + const fullName = ((d.ime||'')+' '+(d.prezime||'')).trim(); + const ggUrl = 'https://www.google.com/search?q='+encodeURIComponent(fullName+' nogometaš HR'); + const wikiUrl = 'https://hr.wikipedia.org/w/index.php?search='+encodeURIComponent(fullName); const html = `
@@ -1922,6 +1981,18 @@ async function openSportas(id){ ${d.broj_dresa?'#'+esc(d.broj_dresa)+'':''} ${d.stipendiran?'STIP':''}
+
+ ${d.visina_cm?''+esc(d.visina_cm)+'cm visina':''} + ${d.tezina_kg?''+esc(d.tezina_kg)+'kg težina':''} + ${d.dominantna_noga?''+esc(d.dominantna_noga)+'noga':''} + ${d.pozicija?''+esc(d.pozicija)+'':''} + ${d.broj_dresa?'#'+esc(d.broj_dresa)+'':''} +
+
@@ -1935,14 +2006,15 @@ async function openSportas(id){
-
Sezone (${sezone.length})
-
Utakmice (${utakmice.length})
+
🏆 HNS Karijera (${sezone.length})
+
📅 Posljednje utakmice (${utakmice.length})
Bio
Godišnjaci (${godisnjaci.length})
Nagrade (${nagrade.length})
+
🏆 HNS Karijera ${sezone.length} sezon${sezone.length===1?'a':(sezone.length<5&&sezone.length>1?'e':'a')}
${sezone.length ? `
${sezone.map(s => ` @@ -1962,6 +2034,7 @@ async function openSportas(id){
SezonaNatjecanjeKlubNas.Gol.Asis.ŽutiCrv.
${utakmice.map(u => `
DatumNatjecanjeSusretGol.Min.