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:
@@ -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}
|
||||
|
||||
|
||||
+130
-2
@@ -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
|
||||
|
||||
+94
-15
@@ -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>
|
||||
|
||||
+228
-12
@@ -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,
|
||||
|
||||
@@ -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'}
|
||||
]}
|
||||
];
|
||||
|
||||
|
||||
+115
-42
@@ -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 '<button class="btn '+(on?'primary':'')+'" '
|
||||
+ 'title="Prikaži samo PGŽ-financirane / u godišnjaku" '
|
||||
+ 'onclick="togglePGZFilter(\''+section+'\')">'
|
||||
+ (on ? '⭐ PGŽ filter ON' : '☆ PGŽ filter OFF') + '</button>';
|
||||
};
|
||||
|
||||
|
||||
//=========== 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 = '<div class="loading">Učitavanje saveza…</div>';
|
||||
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='<div class="empty">Greška pri dohvatu</div>'; return; }
|
||||
_cache.savezi = d.rows || [];
|
||||
}
|
||||
@@ -1156,6 +1211,7 @@ function renderSaveziShell(){
|
||||
<button id="sav-card" class="${_state.viewSavezi==='card'?'active':''}" onclick="setSaveziView('card')">Kartice</button>
|
||||
<button id="sav-table" class="${_state.viewSavezi==='table'?'active':''}" onclick="setSaveziView('table')">Tablica</button>
|
||||
</div>
|
||||
${window.renderPGZToggleBtn ? window.renderPGZToggleBtn('savezi') : ''}
|
||||
<span class="tb-s" id="sav-cnt"></span>
|
||||
</div>
|
||||
<div id="sav-out"></div>
|
||||
@@ -1191,7 +1247,7 @@ function renderSaveziGrid(rows){
|
||||
return '<div class="grid">'+rows.map(s => `
|
||||
<div class="entity" onclick="openSavez(${s.id})">
|
||||
${s.pgz_relevant?'<div class="et-tag">PGŽ</div>':''}
|
||||
<div class="et">${esc(s.naziv)}</div>
|
||||
<div class="et">${(window.pgzBadgePrefix?window.pgzBadgePrefix(s,'savez'):'')}${esc(s.naziv)}</div>
|
||||
<div class="es">${txt(s.sport,'—')} · ${txt(s.predsjednik,'bez predsjednika')}</div>
|
||||
<div class="em">
|
||||
<span><b>${fmtNum(s.broj_klubova)}</b> klubova</span>
|
||||
@@ -1206,7 +1262,7 @@ function renderSaveziTable(rows){
|
||||
<thead><tr>${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Ž','')}</tr></thead>
|
||||
<tbody>${rows.map(s => `
|
||||
<tr onclick="openSavez(${s.id})">
|
||||
<td><b>${esc(s.naziv)}</b></td>
|
||||
<td><b>${(window.pgzBadgePrefix?window.pgzBadgePrefix(s,'savez'):'')}${esc(s.naziv)}</b></td>
|
||||
<td>${txt(s.sport)}</td>
|
||||
<td>${txt(s.predsjednik)}</td>
|
||||
<td>${s.email?'<span class="tag b">'+esc(s.email)+'</span>':'—'}</td>
|
||||
@@ -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 = `
|
||||
<label style="margin-right:12px;cursor:pointer;color:var(--t1)">
|
||||
<input type="checkbox" ${f.financiran?'checked':''} onchange="toggleKlubFilter('financiran')" data-filter="financiran">
|
||||
💰 Samo financirani od PGŽ
|
||||
</label>
|
||||
<label style="margin-right:12px;cursor:pointer;color:var(--t1)">
|
||||
<input type="checkbox" ${f.godisnjak?'checked':''} onchange="toggleKlubFilter('godisnjak')" data-filter="godisnjak">
|
||||
📖 U godišnjaku
|
||||
</label>
|
||||
<label style="cursor:pointer;color:var(--t1)">
|
||||
<input type="checkbox" ${f.priority_only?'checked':''} onchange="toggleKlubFilter('priority_only')" data-filter="priority_only">
|
||||
⭐ Samo prioritet (financiran ili godišnjak)
|
||||
</label>
|
||||
`;
|
||||
};
|
||||
|
||||
function loadKlubovi(){
|
||||
async function loadKlubovi(){
|
||||
const root = $('#pg-klubovi');
|
||||
if(!_cache.klubovi){
|
||||
root.innerHTML = '<div class="loading">Učitavanje klubova…</div>';
|
||||
// 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='<div class="empty">Greška pri dohvatu</div>'; return; }
|
||||
_cache.klubovi = d.rows || [];
|
||||
}
|
||||
@@ -1338,6 +1372,7 @@ function renderKluboviShell(){
|
||||
<button class="btn" onclick="exportKlubovi('xlsx')">⬇ XLSX</button>
|
||||
<button class="btn" onclick="exportKlubovi('csv')">⬇ CSV</button>
|
||||
<button class="btn" onclick="enrichBulk('klub', 50, 70)">✨ Obogati (50)</button>
|
||||
${window.renderPGZToggleBtn ? window.renderPGZToggleBtn('klubovi') : ''}
|
||||
<span class="tb-s" id="kl-cnt"></span>
|
||||
</div>
|
||||
<div id="kl-out"></div>
|
||||
@@ -1388,7 +1423,7 @@ function renderKluboviGrid(rows){
|
||||
return '<div class="grid-club">'+rows.map(k => `
|
||||
<div class="entity" onclick="openKlub(${k.id})">
|
||||
${k.priority?'<div class="et-tag" style="background:#ffd700;color:#1a1a1a">★ PRIO</div>':(k.nositelj_kvalitete?'<div class="et-tag">N.K.</div>':'')}
|
||||
<div class="et">${esc(k.klub||k.sport||'(bez naziva)')}</div>
|
||||
<div class="et">${(window.pgzBadgePrefix?window.pgzBadgePrefix(k,'klub'):'')}${esc(k.klub||k.sport||'(bez naziva)')}</div>
|
||||
<div class="es">${txt(k.razina,'')} · ${txt(k.grad,'—')}</div>
|
||||
<div class="em">
|
||||
${k.financiran?'<span class="tag gd" title="PGŽ sufinanciran">€</span>':''}
|
||||
@@ -1407,7 +1442,7 @@ function renderKluboviTable(rows){
|
||||
<tr>
|
||||
<td onclick="event.stopPropagation()"><input type="checkbox" class="kl-pick" data-id="${k.id}"></td>
|
||||
<td onclick="openKlub(${k.id})">${k.priority?'<span class="tag gd" title="financiran ili u godišnjaku">★</span>':''}</td>
|
||||
<td onclick="openKlub(${k.id})"><b>${esc(k.klub||k.sport||'(bez naziva)')}</b></td>
|
||||
<td onclick="openKlub(${k.id})"><b>${(window.pgzBadgePrefix?window.pgzBadgePrefix(k,'klub'):'')}${esc(k.klub||k.sport||'(bez naziva)')}</b></td>
|
||||
<td onclick="openKlub(${k.id})">${txt(k.sport)}</td>
|
||||
<td onclick="openKlub(${k.id})">${txt(k.razina)}</td>
|
||||
<td onclick="openKlub(${k.id})">${txt(k.grad)}</td>
|
||||
@@ -1678,7 +1713,11 @@ async function loadSportasi(){
|
||||
const root = $('#pg-sportasi');
|
||||
if(!_cache.clanovi){
|
||||
root.innerHTML = '<div class="loading">Učitavanje sportaša…</div>';
|
||||
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='<div class="empty">Greška pri dohvatu</div>'; return; }
|
||||
_cache.clanovi = d.rows || [];
|
||||
}
|
||||
@@ -1718,6 +1757,7 @@ function renderSportasiShell(){
|
||||
<button id="sp-card" class="${_state.viewSportasi==='card'?'active':''}" onclick="setSportasiView('card')">Kartice</button>
|
||||
<button id="sp-table" class="${_state.viewSportasi==='table'?'active':''}" onclick="setSportasiView('table')">Tablica</button>
|
||||
</div>
|
||||
${window.renderPGZToggleBtn ? window.renderPGZToggleBtn('sportasi') : ''}
|
||||
<span class="tb-s" id="sp-cnt"></span>
|
||||
<button class="btn" onclick="enrichBulk('sportas', 50, 70)">✨ Obogati sve (50)</button>
|
||||
<button class="btn primary" onclick="openSportas(449)">⭐ Test: Josip Zec</button>
|
||||
@@ -1855,7 +1895,7 @@ function buildPlayerCard(c){
|
||||
<div class="player-card" onclick="openSportas(${c.id})">
|
||||
<div class="ph">${photo}</div>
|
||||
<div class="pb">
|
||||
<div class="pn">${esc(c.ime||'')} ${esc(c.prezime||'')}</div>
|
||||
<div class="pn">${(window.pgzBadgePrefix?window.pgzBadgePrefix(c,'sportas'):'')}${esc(c.ime||'')} ${esc(c.prezime||'')}</div>
|
||||
<div class="pp">${txt(c.sport,'—')} · ${txt(c.pozicija,'')}</div>
|
||||
<div class="pk">${txt(c.klub_naziv_godisnjak,'')}</div>
|
||||
<div class="badges">
|
||||
@@ -1873,7 +1913,7 @@ function renderSportasiTable(rows){
|
||||
<thead><tr>${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','')}</tr></thead>
|
||||
<tbody>${rows.map(c => `
|
||||
<tr onclick="openSportas(${c.id})">
|
||||
<td><b>${esc(c.prezime||'')}</b></td>
|
||||
<td><b>${(window.pgzBadgePrefix?window.pgzBadgePrefix(c,'sportas'):'')}${esc(c.prezime||'')}</b></td>
|
||||
<td>${esc(c.ime||'')}</td>
|
||||
<td>${txt(c.sport)}</td>
|
||||
<td>${txt(c.pozicija)}</td>
|
||||
@@ -1886,11 +1926,23 @@ function renderSportasiTable(rows){
|
||||
|
||||
async function openSportas(id){
|
||||
openPanel('Sportaš', '<div class="loading">Učitavanje profila…</div>');
|
||||
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š', '<div class="empty">Sportaš nije pronađen</div>');
|
||||
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 ? '<img src="'+esc(d.slika_url)+'" alt="" onerror="this.style.display=\'none\';if(this.parentElement)this.parentElement.innerHTML=\'<div class=\\\'no\\\'>'+initials+'</div>\'">' : '<div class="no">'+initials+'</div>';
|
||||
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 = `
|
||||
<div class="pp-hdr">
|
||||
@@ -1922,6 +1981,18 @@ async function openSportas(id){
|
||||
${d.broj_dresa?'<span class="tag">#'+esc(d.broj_dresa)+'</span>':''}
|
||||
${d.stipendiran?'<a class="tag am" onclick="filterSportasiBy("stipendiran",true)">STIP</a>':''}
|
||||
</div>
|
||||
<div class="pp-bio-row">
|
||||
${d.visina_cm?'<span class="pp-bio-chip"><b>'+esc(d.visina_cm)+'</b>cm visina</span>':''}
|
||||
${d.tezina_kg?'<span class="pp-bio-chip"><b>'+esc(d.tezina_kg)+'</b>kg težina</span>':''}
|
||||
${d.dominantna_noga?'<span class="pp-bio-chip"><b>'+esc(d.dominantna_noga)+'</b>noga</span>':''}
|
||||
${d.pozicija?'<span class="pp-bio-chip"><b>'+esc(d.pozicija)+'</b></span>':''}
|
||||
${d.broj_dresa?'<span class="pp-bio-chip">#<b>'+esc(d.broj_dresa)+'</b></span>':''}
|
||||
</div>
|
||||
<div class="pp-links">
|
||||
${hnsUrl?'<a class="pp-link hns" href="'+esc(hnsUrl)+'" target="_blank" rel="noopener">⚽ HNS profil ↗</a>':''}
|
||||
<a class="pp-link gg" href="${esc(ggUrl)}" target="_blank" rel="noopener">🔍 Google</a>
|
||||
<a class="pp-link wiki" href="${esc(wikiUrl)}" target="_blank" rel="noopener">📖 Wikipedia</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1935,14 +2006,15 @@ async function openSportas(id){
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tab active" onclick="switchPlayerTab(this,'p-sez')">Sezone (${sezone.length})</div>
|
||||
<div class="tab" onclick="switchPlayerTab(this,'p-utak')">Utakmice (${utakmice.length})</div>
|
||||
<div class="tab active" onclick="switchPlayerTab(this,'p-sez')">🏆 HNS Karijera (${sezone.length})</div>
|
||||
<div class="tab" onclick="switchPlayerTab(this,'p-utak')">📅 Posljednje utakmice (${utakmice.length})</div>
|
||||
<div class="tab" onclick="switchPlayerTab(this,'p-bio')">Bio</div>
|
||||
<div class="tab" onclick="switchPlayerTab(this,'p-god')">Godišnjaci (${godisnjaci.length})</div>
|
||||
<div class="tab" onclick="switchPlayerTab(this,'p-nag')">Nagrade (${nagrade.length})</div>
|
||||
</div>
|
||||
|
||||
<div id="p-sez" class="ptab">
|
||||
<div class="pp-section-h">🏆 HNS Karijera <span class="cnt">${sezone.length} sezon${sezone.length===1?'a':(sezone.length<5&&sezone.length>1?'e':'a')}</span></div>
|
||||
${sezone.length ? `<div style="overflow-x:auto"><table>
|
||||
<thead><tr><th>Sezona</th><th>Natjecanje</th><th>Klub</th><th class="num">Nas.</th><th class="num">Gol.</th><th class="num">Asis.</th><th class="num">Žuti</th><th class="num">Crv.</th><th></th></tr></thead>
|
||||
<tbody>${sezone.map(s => `
|
||||
@@ -1962,6 +2034,7 @@ async function openSportas(id){
|
||||
</div>
|
||||
|
||||
<div id="p-utak" class="ptab" style="display:none">
|
||||
<div class="pp-section-h">📅 Posljednje utakmice <span class="cnt">${utakmice.length} zabilježeno</span></div>
|
||||
${utakmice.length ? `<div style="overflow-x:auto;max-height:500px;overflow-y:auto"><table>
|
||||
<thead><tr><th>Datum</th><th>Natjecanje</th><th colspan="3">Susret</th><th class="num">Gol.</th><th class="num">Min.</th><th></th></tr></thead>
|
||||
<tbody>${utakmice.map(u => `
|
||||
|
||||
Reference in New Issue
Block a user