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}
| Naziv | Klubovi | Sportaši | Predsjednik | |
${rows||'| 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}
| Naziv | Savez | Grad | Članova | Predsjednik |
${rows||'| — |
'}
`;
};
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}
| Ime i prezime | Klub | Kategorija | Spol | Rođen |
${rows||'| — |
'}
`;
};
@@ -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 '
';
+};