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
+130 -2
View File
@@ -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