HNS sprint: 3-tab drill-down + parallel deep scraper dispatch
HNS-1 verify: smoke test 93409 OK, gap 854 uncovered, throughput ~60/min HNS-2 dispatch: scripts/hns_dispatch.sh + 5 parallel workers shard'd po roster ID; coverage 265→1098 distinct_seasons (93.7% of 1172 roster), 125→971 distinct_matches; total seasons 3170→13371, matches 23515→150071 HNS-3 UI: 6-tab panel collapsed na 3 (🏆 Karijera / 📅 Utakmice / 👤 Profil); novi /api/v2/clan/{id}/hns-matches?limit=30 + /clan/{id}/hns-profile (bio + summary + HNS deep link); prof-grid 3-col card s gold jersey badge; OIB RBAC-mask. Test Josip Zec 449: 72 sez/30 utak. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5856,3 +5856,87 @@ def v2_clanovi_priority_sort(only: bool = False, limit: int = 500):
|
|||||||
""", (limit,))
|
""", (limit,))
|
||||||
return {"count": len(rows), "rows": rows}
|
return {"count": len(rows), "rows": rows}
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# HNS-3 (2026-05-05) — explicit drill-down endpoints for sport2.html
|
||||||
|
# Author: Damir Radulić (dradulic@outlook.com / damir@rinet.one)
|
||||||
|
# Description: 3-tab drill-down (HNS Karijera / Utakmice / Profil)
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
@router.get("/clan/{clan_id}/hns-matches")
|
||||||
|
def v2_clan_hns_matches(clan_id: int, limit: int = 30):
|
||||||
|
"""Posljednje N utakmica iz hns_player_matches za sportaša (sortirano DESC po datumu)."""
|
||||||
|
if limit < 1: limit = 1
|
||||||
|
if limit > 500: limit = 500
|
||||||
|
rows = db_query("""
|
||||||
|
SELECT id, hns_igrac_id, clan_id, datum, natjecanje,
|
||||||
|
domacin, gost, rezultat, pozicija, startna,
|
||||||
|
minute_od, minute_do,
|
||||||
|
CASE WHEN minute_od IS NOT NULL AND minute_do IS NOT NULL
|
||||||
|
THEN (minute_do - minute_od) ELSE NULL END AS minute,
|
||||||
|
golovi, asistencije, zuti, crveni, source_url, scraped_at
|
||||||
|
FROM pgz_sport.hns_player_matches
|
||||||
|
WHERE clan_id = %s
|
||||||
|
ORDER BY datum DESC NULLS LAST, id DESC
|
||||||
|
LIMIT %s
|
||||||
|
""", (clan_id, limit))
|
||||||
|
return {"clan_id": clan_id, "limit": limit, "count": len(rows), "rows": rows}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/clan/{clan_id}/hns-profile")
|
||||||
|
def v2_clan_hns_profile(clan_id: int):
|
||||||
|
"""Bio + HNS profil block za drill-down 'Profil' tab.
|
||||||
|
Vraća sve relevantne kolone iz pgz_sport.clanovi + HNS deep link + agregirane HNS statistike.
|
||||||
|
"""
|
||||||
|
p = db_one("""
|
||||||
|
SELECT c.id, c.ime, c.prezime, c.oib,
|
||||||
|
c.datum_rodenja, c.datum_rodjenja,
|
||||||
|
c.mjesto_rodenja, c.mjesto_rodjenja,
|
||||||
|
c.spol, c.sport, c.pozicija,
|
||||||
|
c.dominantna_noga, c.visina_cm, c.tezina_kg,
|
||||||
|
c.broj_dresa, c.broj_dres,
|
||||||
|
c.kategorija, c.podkategorija, c.kategorije,
|
||||||
|
c.reprezentativac, c.kategoriziran, c.stipendiran,
|
||||||
|
c.aktivan, c.aktivni_status,
|
||||||
|
c.email, c.telefon, c.adresa, c.grad,
|
||||||
|
c.biografija,
|
||||||
|
c.slug, c.slika_url,
|
||||||
|
c.hns_igrac_id, c.profile_url, c.source_url,
|
||||||
|
c.klub_id, c.klub_naziv_godisnjak,
|
||||||
|
k.naziv AS klub_naziv,
|
||||||
|
c.dob_age,
|
||||||
|
EXTRACT(YEAR FROM age(COALESCE(c.datum_rodjenja, c.datum_rodenja)))::int AS dob_calc
|
||||||
|
FROM pgz_sport.clanovi c
|
||||||
|
LEFT JOIN pgz_sport.klubovi k ON k.id = c.klub_id
|
||||||
|
WHERE c.id = %s
|
||||||
|
""", (clan_id,))
|
||||||
|
if not p:
|
||||||
|
raise HTTPException(404, "Sportaš nije pronađen")
|
||||||
|
|
||||||
|
# Aggregate HNS career stats (cheap, single row)
|
||||||
|
summary = db_one("""
|
||||||
|
SELECT count(DISTINCT sezona) AS sezona_broj,
|
||||||
|
COALESCE(sum(nastupi),0) AS ukupno_nastupa,
|
||||||
|
COALESCE(sum(golovi),0) AS ukupno_golova,
|
||||||
|
COALESCE(sum(asistencije),0) AS ukupno_asistencija,
|
||||||
|
COALESCE(sum(zuti),0) AS ukupno_zutih,
|
||||||
|
COALESCE(sum(crveni),0) AS ukupno_crvenih,
|
||||||
|
COALESCE(sum(minute),0) AS ukupno_minuta,
|
||||||
|
min(sezona) AS prva_sezona,
|
||||||
|
max(sezona) AS zadnja_sezona
|
||||||
|
FROM pgz_sport.hns_player_seasons WHERE clan_id = %s
|
||||||
|
""", (clan_id,)) or {}
|
||||||
|
|
||||||
|
# Build HNS deep link
|
||||||
|
hns_url = p.get("profile_url")
|
||||||
|
if not hns_url and p.get("hns_igrac_id"):
|
||||||
|
slug = p.get("slug") or f"{(p.get('ime') or '').lower()}-{(p.get('prezime') or '').lower()}".replace(" ", "-")
|
||||||
|
hns_url = f"https://semafor.hns.family/igraci/{p['hns_igrac_id']}/{slug}/"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"clan_id": clan_id,
|
||||||
|
"profile": p,
|
||||||
|
"hns_summary": summary,
|
||||||
|
"hns_url": hns_url,
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Executable
+49
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# hns_dispatch.sh — parallel HNS player deep-scrape dispatcher
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
# Author: dradulic@outlook.com / damir@rinet.one
|
||||||
|
# Date: 2026-05-05
|
||||||
|
# Version: 1.0
|
||||||
|
# Description:
|
||||||
|
# Reads a shard file of HNS player IDs (one per line) and invokes
|
||||||
|
# /opt/pgz-sport/scripts/hns_player_deep.py --player <ID> --no-telegram
|
||||||
|
# sequentially for each ID. Designed to be launched 5x in parallel
|
||||||
|
# (one per shard) so 5 worker processes each chew through ~175 IDs.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash /opt/pgz-sport/scripts/hns_dispatch.sh <shard_file> <worker_id>
|
||||||
|
# bash /opt/pgz-sport/scripts/hns_dispatch.sh /tmp/hns_shard_aa 1
|
||||||
|
#
|
||||||
|
# Per-worker log: /tmp/hns_worker_<worker_id>.log
|
||||||
|
# Each player attempt: ~1s (HTTP + parse + UPSERT).
|
||||||
|
# Stagger workers manually with `sleep N &&` when launching.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
SHARD_FILE="${1:?usage: hns_dispatch.sh <shard_file> <worker_id>}"
|
||||||
|
WORKER_ID="${2:?usage: hns_dispatch.sh <shard_file> <worker_id>}"
|
||||||
|
LOG="/tmp/hns_worker_${WORKER_ID}.log"
|
||||||
|
SCRIPT="/opt/pgz-sport/scripts/hns_player_deep.py"
|
||||||
|
|
||||||
|
cd /opt/pgz-sport || exit 2
|
||||||
|
|
||||||
|
echo "[$(date -Iseconds)] worker=${WORKER_ID} shard=${SHARD_FILE} start ($(wc -l < "$SHARD_FILE") IDs)" >> "$LOG"
|
||||||
|
|
||||||
|
count=0
|
||||||
|
ok=0
|
||||||
|
fail=0
|
||||||
|
while IFS= read -r ID; do
|
||||||
|
[ -z "$ID" ] && continue
|
||||||
|
count=$((count + 1))
|
||||||
|
if python3 "$SCRIPT" --player "$ID" --no-telegram >> "$LOG" 2>&1; then
|
||||||
|
ok=$((ok + 1))
|
||||||
|
else
|
||||||
|
fail=$((fail + 1))
|
||||||
|
echo "[$(date -Iseconds)] worker=${WORKER_ID} FAIL id=${ID}" >> "$LOG"
|
||||||
|
fi
|
||||||
|
# tiny politeness pause; HNS is light traffic but 5 parallel workers
|
||||||
|
# = 5 req/s aggregate; small jitter helps avoid bursts
|
||||||
|
sleep 0.2
|
||||||
|
done < "$SHARD_FILE"
|
||||||
|
|
||||||
|
echo "[$(date -Iseconds)] worker=${WORKER_ID} done count=${count} ok=${ok} fail=${fail}" >> "$LOG"
|
||||||
@@ -129,6 +129,11 @@ COMP_CATALOG = [
|
|||||||
("100381484", "kvalifikacije-za-prvu-nl-pioniri", "pioniri-u15", "2025/2026"),
|
("100381484", "kvalifikacije-za-prvu-nl-pioniri", "pioniri-u15", "2025/2026"),
|
||||||
("100569152", "treca-nl-istok", "seniori", "2025/2026"), # Treća NL Istok
|
("100569152", "treca-nl-istok", "seniori", "2025/2026"), # Treća NL Istok
|
||||||
("100585203", "treca-nl-zapad", "seniori", "2025/2026"), # Treća NL Zapad (PGŽ klubovi)
|
("100585203", "treca-nl-zapad", "seniori", "2025/2026"), # Treća NL Zapad (PGŽ klubovi)
|
||||||
|
# PGŽ-region ŽNL leagues discovered via klub raspored auto-discovery
|
||||||
|
("101555188", "1-znl-seniori", "seniori", "2025/2026"), # 1.ŽNL PGŽ seniori
|
||||||
|
("112195128", "kup-zns", "seniori", "2025/2026"), # Kup ŽNS Vinodolsko-Senjsko
|
||||||
|
("104425442", "kup-mladezi-juniori", "juniori-u19", "2025/2026"),
|
||||||
|
("104464435", "kup-mladezi-kadeti", "kadeti-u17", "2025/2026"),
|
||||||
("100391485", "supersport-hnl", "seniori", "2025/2026"),
|
("100391485", "supersport-hnl", "seniori", "2025/2026"),
|
||||||
("100413651", "supersport-prva-nl", "seniori", "2025/2026"),
|
("100413651", "supersport-prva-nl", "seniori", "2025/2026"),
|
||||||
("100418001", "supersport-druga-nl", "seniori", "2025/2026"),
|
("100418001", "supersport-druga-nl", "seniori", "2025/2026"),
|
||||||
|
|||||||
+89
-46
@@ -205,6 +205,30 @@ a.tag:hover,.tag[onclick]:hover{transform:translateY(-1px);filter:brightness(1.1
|
|||||||
|
|
||||||
.utlogo{width:22px;height:22px;border-radius:50%;background:var(--bg3);object-fit:contain;vertical-align:middle;margin-right:6px}
|
.utlogo{width:22px;height:22px;border-radius:50%;background:var(--bg3);object-fit:contain;vertical-align:middle;margin-right:6px}
|
||||||
|
|
||||||
|
/* HNS-3 (2026-05-05) — Profil tab styles (Palantir Gotham aesthetic) */
|
||||||
|
.prof-top{display:flex;align-items:flex-start;gap:18px;padding:14px;background:var(--bg2);border:1px solid var(--rim);border-radius:6px;margin-bottom:14px}
|
||||||
|
.prof-name-block{flex:1;min-width:0}
|
||||||
|
.prof-name{font-size:20px;font-weight:800;color:var(--t0);letter-spacing:.3px;margin-bottom:4px}
|
||||||
|
.prof-sub{color:var(--t2);font-size:12px;margin-bottom:6px}
|
||||||
|
.prof-club{font-size:12.5px;color:var(--t1);display:flex;align-items:center;flex-wrap:wrap;gap:6px}
|
||||||
|
.prof-dres{flex-shrink:0;width:96px;height:96px;border:2px solid var(--pgz-gold);border-radius:8px;display:flex;flex-direction:column;align-items:center;justify-content:center;background:linear-gradient(135deg,var(--bg3),var(--bg2))}
|
||||||
|
.prof-dres-num{font-size:38px;font-weight:900;color:var(--pgz-gold);line-height:1;font-family:var(--mono)}
|
||||||
|
.prof-dres-l{font-size:9.5px;color:var(--t3);letter-spacing:1.5px;font-weight:700;margin-top:4px}
|
||||||
|
.prof-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:6px}
|
||||||
|
.prof-cell{padding:9px 12px;background:var(--bg2);border:1px solid var(--rim);border-radius:5px}
|
||||||
|
.prof-cell .l{font-size:10px;color:var(--t3);text-transform:uppercase;letter-spacing:.5px;font-weight:700;margin-bottom:3px}
|
||||||
|
.prof-cell .v{font-size:13px;color:var(--t0);font-weight:600;word-break:break-word}
|
||||||
|
.prof-cell .v b{color:var(--pgz-gold);font-family:var(--mono)}
|
||||||
|
@media (max-width:760px){
|
||||||
|
.prof-grid{grid-template-columns:repeat(2,1fr)}
|
||||||
|
.prof-top{flex-direction:column-reverse;align-items:stretch}
|
||||||
|
.prof-dres{width:100%;height:78px;flex-direction:row;gap:14px}
|
||||||
|
.prof-dres-num{font-size:32px}
|
||||||
|
}
|
||||||
|
@media (max-width:420px){
|
||||||
|
.prof-grid{grid-template-columns:1fr}
|
||||||
|
}
|
||||||
|
|
||||||
.score{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:4px;background:var(--bg3);font-size:11px;font-weight:600}
|
.score{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:4px;background:var(--bg3);font-size:11px;font-weight:600}
|
||||||
.score.high{background:rgba(0,232,143,.15);color:var(--green)}
|
.score.high{background:rgba(0,232,143,.15);color:var(--green)}
|
||||||
.score.mid{background:rgba(245,158,11,.15);color:var(--amber)}
|
.score.mid{background:rgba(245,158,11,.15);color:var(--amber)}
|
||||||
@@ -1973,7 +1997,7 @@ async function openSportas(id){
|
|||||||
const d = Object.assign({}, dRaw);
|
const d = Object.assign({}, dRaw);
|
||||||
if(dV2 && dV2.profile){
|
if(dV2 && dV2.profile){
|
||||||
const p = 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']
|
['visina_cm','tezina_kg','dominantna_noga','broj_dresa','biografija','mjesto_rodenja','mjesto_rodjenja','datum_rodenja','datum_rodjenja','spol','sport','aktivan','pozicija','klub_naziv','klub_id','dob_age','hns_igrac_id','profile_url','reprezentativac','stipendiran','datum_pristupa','email','telefon','oib','slug','slika_url']
|
||||||
.forEach(k => { if((d[k]===null||d[k]===undefined||d[k]==='') && p[k]!==null && p[k]!==undefined && p[k]!=='') d[k]=p[k]; });
|
.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 stats = d.stats || {};
|
||||||
@@ -2040,40 +2064,39 @@ async function openSportas(id){
|
|||||||
<div class="pp-stat"><div class="v">${fmtNum(stats.sezone_aktivne||sezone.length)}</div><div class="l">Sezona</div></div>
|
<div class="pp-stat"><div class="v">${fmtNum(stats.sezone_aktivne||sezone.length)}</div><div class="l">Sezona</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- HNS-3 (2026-05-05) — 3 explicit tabs: HNS Karijera / Utakmice (last 30) / Profil -->
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<div class="tab active" onclick="switchPlayerTab(this,'p-sez')">🏆 HNS Karijera (${sezone.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-utak')">📅 Utakmice (poslj. ${Math.min(utakmice.length,30)})</div>
|
||||||
<div class="tab" onclick="switchPlayerTab(this,'p-kat')">🏷️ Kategorije (${kategorije.length})</div>
|
<div class="tab" onclick="switchPlayerTab(this,'p-prof')">👤 Profil</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>
|
||||||
|
|
||||||
<div id="p-sez" class="ptab">
|
<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>
|
<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>
|
${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>
|
<thead><tr><th>Sezona</th><th>Klub</th><th>Natjecanje</th><th class="num">Nastupi</th><th class="num">Golovi</th><th class="num">Asis.</th><th class="num">Žuti</th><th class="num">Crv.</th><th class="num">Min.</th><th></th></tr></thead>
|
||||||
<tbody>${sezone.map(s => `
|
<tbody>${[...sezone].sort((a,b)=>String(b.sezona||'').localeCompare(String(a.sezona||''))).map(s => `
|
||||||
<tr class="no-click">
|
<tr class="no-click">
|
||||||
<td><b>${esc(s.sezona||'')}</b></td>
|
<td><b>${esc(s.sezona||'')}</b></td>
|
||||||
<td>${esc(s.natjecanje||'')}</td>
|
|
||||||
<td>${esc(s.klub_naziv||'')}</td>
|
<td>${esc(s.klub_naziv||'')}</td>
|
||||||
|
<td>${esc(s.natjecanje||'')}</td>
|
||||||
<td class="num">${fmtNum(s.nastupi)}</td>
|
<td class="num">${fmtNum(s.nastupi)}</td>
|
||||||
<td class="num"><b style="color:var(--pgz-gold)">${fmtNum(s.pogoci ?? s.golovi)}</b></td>
|
<td class="num"><b style="color:var(--pgz-gold)">${fmtNum(s.pogoci ?? s.golovi)}</b></td>
|
||||||
<td class="num">${fmtNum(s.asistencije)}</td>
|
<td class="num">${fmtNum(s.asistencije)}</td>
|
||||||
<td class="num">${fmtNum(s.zuti_kartoni ?? s.zuti)}</td>
|
<td class="num" style="color:var(--amber)">${fmtNum(s.zuti_kartoni ?? s.zuti)}</td>
|
||||||
<td class="num">${fmtNum(s.crveni_kartoni ?? s.crveni)}</td>
|
<td class="num" style="color:var(--red)">${fmtNum(s.crveni_kartoni ?? s.crveni)}</td>
|
||||||
|
<td class="num">${fmtNum(s.minute)}</td>
|
||||||
<td>${(s.natjecanje_url||s.source_url)?'<a href="'+esc(s.natjecanje_url||s.source_url)+'" target="_blank">↗</a>':''}</td>
|
<td>${(s.natjecanje_url||s.source_url)?'<a href="'+esc(s.natjecanje_url||s.source_url)+'" target="_blank">↗</a>':''}</td>
|
||||||
</tr>`).join('')}
|
</tr>`).join('')}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table></div>` : '<div class="empty">Nema sezonskih podataka</div>'}
|
</table></div>` : '<div class="empty">Nema sezonskih podataka iz HNS Semafora</div>'}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="p-utak" class="ptab" style="display:none">
|
<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>
|
<div class="pp-section-h">📅 Utakmice <span class="cnt">posljednjih ${Math.min(utakmice.length,30)} ${utakmice.length===1?'utakmica':(utakmice.length<5&&utakmice.length>1?'utakmice':'utakmica')}</span></div>
|
||||||
${utakmice.length ? `<div style="overflow-x:auto;max-height:500px;overflow-y:auto"><table>
|
${utakmice.length ? `<div style="overflow-x:auto;max-height:560px;overflow-y:auto"><table>
|
||||||
<thead><tr><th>Datum</th><th>Natjecanje</th><th colspan="3">Susret</th><th>Pozicija</th><th class="num">Gol.</th><th class="num">Min.</th><th></th></tr></thead>
|
<thead><tr><th>Datum</th><th>Natjecanje</th><th colspan="3">Susret</th><th>Pozicija</th><th class="num">Min.</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>${utakmice.map(u => {
|
<tbody>${utakmice.slice(0,30).map(u => {
|
||||||
const dom = u.klub_dom || u.domacin || '';
|
const dom = u.klub_dom || u.domacin || '';
|
||||||
const gost = u.klub_gost || u.gost || '';
|
const gost = u.klub_gost || u.gost || '';
|
||||||
const golovi = (u.pogodaka != null ? u.pogodaka : u.golovi);
|
const golovi = (u.pogodaka != null ? u.pogodaka : u.golovi);
|
||||||
@@ -2086,8 +2109,11 @@ async function openSportas(id){
|
|||||||
<td><b>${esc(u.rezultat||'-')}</b></td>
|
<td><b>${esc(u.rezultat||'-')}</b></td>
|
||||||
<td>${u.klub_gost_logo?'<img src="'+esc(u.klub_gost_logo)+'" class="utlogo" onerror="this.style.display=\'none\'">':''}${esc(gost)}</td>
|
<td>${u.klub_gost_logo?'<img src="'+esc(u.klub_gost_logo)+'" class="utlogo" onerror="this.style.display=\'none\'">':''}${esc(gost)}</td>
|
||||||
<td>${esc(u.pozicija||'—')}</td>
|
<td>${esc(u.pozicija||'—')}</td>
|
||||||
<td class="num"><b style="color:var(--pgz-gold)">${fmtNum(golovi)}</b></td>
|
|
||||||
<td class="num">${fmtNum(minute)}</td>
|
<td class="num">${fmtNum(minute)}</td>
|
||||||
|
<td class="num"><b style="color:var(--pgz-gold)">${fmtNum(golovi)}</b></td>
|
||||||
|
<td class="num">${fmtNum(u.asistencije)}</td>
|
||||||
|
<td class="num" style="color:var(--amber)">${fmtNum(u.zuti)}</td>
|
||||||
|
<td class="num" style="color:var(--red)">${fmtNum(u.crveni)}</td>
|
||||||
<td>${u.source_url?'<a href="'+esc(u.source_url)+'" target="_blank">↗</a>':''}</td>
|
<td>${u.source_url?'<a href="'+esc(u.source_url)+'" target="_blank">↗</a>':''}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('')}
|
}).join('')}
|
||||||
@@ -2095,9 +2121,46 @@ async function openSportas(id){
|
|||||||
</table></div>` : '<div class="empty">Nema podataka o utakmicama</div>'}
|
</table></div>` : '<div class="empty">Nema podataka o utakmicama</div>'}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="p-kat" class="ptab" style="display:none">
|
<div id="p-prof" class="ptab" style="display:none">
|
||||||
<div class="pp-section-h">🏷️ Kategorije <span class="cnt">${kategorije.length} ${kategorije.length===1?'zapis':(kategorije.length<5&&kategorije.length>1?'zapisa':'zapisa')}</span></div>
|
<div class="pp-section-h">👤 Profil <span class="cnt">${esc(d.ime||'')} ${esc(d.prezime||'')}</span></div>
|
||||||
${kategorije.length ? `<div style="overflow-x:auto;max-height:500px;overflow-y:auto"><table>
|
|
||||||
|
<!-- Top: name + dres + active club + HNS deep link -->
|
||||||
|
<div class="prof-top">
|
||||||
|
<div class="prof-name-block">
|
||||||
|
<div class="prof-name">${esc(d.ime||'')} ${esc(d.prezime||'')}</div>
|
||||||
|
<div class="prof-sub">
|
||||||
|
${dob?'📅 '+fmtDate(dob)+(d.dob_age?' · '+d.dob_age+' god.':(dob?(' · '+(new Date().getFullYear()-Number(String(dob).slice(0,4)))+' god.'):'')):'—'}
|
||||||
|
${(d.mjesto_rodjenja||d.mjesto_rodenja)?' · '+esc(d.mjesto_rodjenja||d.mjesto_rodenja):''}
|
||||||
|
</div>
|
||||||
|
<div class="prof-club">
|
||||||
|
${d.klub_id ? '<a class="link-chip" onclick="closePanel();setTimeout(()=>openKlub('+d.klub_id+'),250)">🏟️ '+esc(d.klub_naziv_full||d.klub_naziv||d.klub_naziv_godisnjak||'—')+'</a>' : '🏟️ '+esc(d.klub_naziv_full||d.klub_naziv||d.klub_naziv_godisnjak||'—')}
|
||||||
|
${d.aktivan?'<span class="tag gr" style="margin-left:8px">AKTIVAN</span>':'<span class="tag rd" style="margin-left:8px">NEAKTIVAN</span>'}
|
||||||
|
</div>
|
||||||
|
${hnsUrl?'<div style="margin-top:10px"><a class="pp-link hns" href="'+esc(hnsUrl)+'" target="_blank" rel="noopener">⚽ HNS Semafor profil ↗</a></div>':''}
|
||||||
|
</div>
|
||||||
|
${d.broj_dresa?'<div class="prof-dres"><div class="prof-dres-num">'+esc(d.broj_dresa)+'</div><div class="prof-dres-l">DRES</div></div>':''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bio grid -->
|
||||||
|
<div class="prof-grid">
|
||||||
|
<div class="prof-cell"><div class="l">Pozicija</div><div class="v">${txt(d.pozicija)}</div></div>
|
||||||
|
<div class="prof-cell"><div class="l">Dominantna noga</div><div class="v">${txt(d.dominantna_noga)}</div></div>
|
||||||
|
<div class="prof-cell"><div class="l">Visina</div><div class="v">${d.visina_cm?'<b>'+d.visina_cm+'</b> cm':'—'}</div></div>
|
||||||
|
<div class="prof-cell"><div class="l">Težina</div><div class="v">${d.tezina_kg?'<b>'+d.tezina_kg+'</b> kg':'—'}</div></div>
|
||||||
|
<div class="prof-cell"><div class="l">Datum rođenja</div><div class="v">${fmtDate(dob)}</div></div>
|
||||||
|
<div class="prof-cell"><div class="l">Spol</div><div class="v">${txt(d.spol)}</div></div>
|
||||||
|
<div class="prof-cell"><div class="l">OIB</div><div class="v">${d.oib?(canSeeFullOib({klub_id:d.klub_id,savez_id:d.savez_id})?'<a class="link-chip" onclick="openOIB("'+esc(d.oib)+'")">'+esc(d.oib)+'</a>':maskOib(d.oib)):'—'}</div></div>
|
||||||
|
<div class="prof-cell"><div class="l">Email</div><div class="v">${d.email?'<a href="mailto:'+esc(d.email)+'">'+esc(d.email)+'</a>':'—'}</div></div>
|
||||||
|
<div class="prof-cell"><div class="l">Telefon</div><div class="v">${txt(d.telefon)}</div></div>
|
||||||
|
<div class="prof-cell"><div class="l">HNS ID</div><div class="v">${hnsId?esc(hnsId):'—'}</div></div>
|
||||||
|
<div class="prof-cell"><div class="l">Datum pristupa</div><div class="v">${fmtDate(d.datum_pristupa)}</div></div>
|
||||||
|
<div class="prof-cell"><div class="l">Status</div><div class="v">${d.aktivan?'<span class="tag gr">AKTIVAN</span>':'<span class="tag rd">NEAKTIVAN</span>'}${d.reprezentativac?' <span class="tag gd">REPR</span>':''}${d.stipendiran?' <span class="tag am">STIP</span>':''}</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${d.biografija ? '<div class="card" style="margin-top:14px"><div class="card-t">Biografija</div><div style="font-size:12px;line-height:1.5;color:var(--t1);margin-top:6px">'+esc(d.biografija)+'</div></div>' : ''}
|
||||||
|
|
||||||
|
${kategorije.length ? `<div class="pp-section-h" style="margin-top:18px">🏷️ Kategorije <span class="cnt">${kategorije.length}</span></div>
|
||||||
|
<div style="overflow-x:auto;max-height:260px;overflow-y:auto"><table>
|
||||||
<thead><tr><th>Sezona</th><th>Kategorija</th><th>Klub</th><th>Izvor</th><th></th></tr></thead>
|
<thead><tr><th>Sezona</th><th>Kategorija</th><th>Klub</th><th>Izvor</th><th></th></tr></thead>
|
||||||
<tbody>${kategorije.map(k => `
|
<tbody>${kategorije.map(k => `
|
||||||
<tr class="no-click">
|
<tr class="no-click">
|
||||||
@@ -2108,37 +2171,17 @@ async function openSportas(id){
|
|||||||
<td>${k.source_url?'<a href="'+esc(k.source_url)+'" target="_blank" rel="noopener">↗</a>':''}</td>
|
<td>${k.source_url?'<a href="'+esc(k.source_url)+'" target="_blank" rel="noopener">↗</a>':''}</td>
|
||||||
</tr>`).join('')}
|
</tr>`).join('')}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table></div>` : '<div class="empty">Nema M2M kategorija (clan_kategorije)</div>'}
|
</table></div>` : ''}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="p-bio" class="ptab" style="display:none">
|
${godisnjaci.length ? `<div class="pp-section-h" style="margin-top:18px">📚 Godišnjaci <span class="cnt">${godisnjaci.length}</span></div>
|
||||||
<div class="kv">
|
<div class="kv">
|
||||||
<div class="k">OIB</div><div class="v">${d.oib?(canSeeFullOib({klub_id:d.klub_id,savez_id:d.savez_id})?'<a class="link-chip" onclick="openOIB("'+esc(d.oib)+'")">'+esc(d.oib)+'</a>':maskOib(d.oib)):'—'}</div>
|
|
||||||
<div class="k">Datum rođenja</div><div class="v">${fmtDate(dob)}</div>
|
|
||||||
<div class="k">Mjesto rođenja</div><div class="v">${txt(d.mjesto_rodjenja||d.mjesto_rodenja)}</div>
|
|
||||||
<div class="k">Spol</div><div class="v">${txt(d.spol)}</div>
|
|
||||||
<div class="k">Visina</div><div class="v">${d.visina_cm?d.visina_cm+' cm':'—'}</div>
|
|
||||||
<div class="k">Težina</div><div class="v">${d.tezina_kg?d.tezina_kg+' kg':'—'}</div>
|
|
||||||
<div class="k">Dom. noga</div><div class="v">${txt(d.dominantna_noga)}</div>
|
|
||||||
<div class="k">Status</div><div class="v">${d.aktivan?'AKTIVAN':'NEAKTIVAN'}</div>
|
|
||||||
<div class="k">Datum pristupa</div><div class="v">${fmtDate(d.datum_pristupa)}</div>
|
|
||||||
<div class="k">Email</div><div class="v">${d.email?'<a href="mailto:'+esc(d.email)+'">'+esc(d.email)+'</a>':'—'}</div>
|
|
||||||
<div class="k">Telefon</div><div class="v">${txt(d.telefon)}</div>
|
|
||||||
<div class="k">Profil</div><div class="v">${(d.profile_url||d.scrape_url)?'<a href="'+esc(d.profile_url||d.scrape_url)+'" target="_blank">↗ vanjski profil</a>':'—'}</div>
|
|
||||||
</div>
|
|
||||||
${d.biografija ? '<div class="card" style="margin-top:14px"><div class="card-t">Biografija</div><div style="font-size:12px;line-height:1.5;color:var(--t1);margin-top:6px">'+esc(d.biografija)+'</div></div>' : ''}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="p-god" class="ptab" style="display:none">
|
|
||||||
${godisnjaci.length ? `<div class="kv">
|
|
||||||
<div class="k">Prvi godišnjak</div><div class="v">${esc(d.godisnjak_prvi||godisnjaci[0])}</div>
|
<div class="k">Prvi godišnjak</div><div class="v">${esc(d.godisnjak_prvi||godisnjaci[0])}</div>
|
||||||
<div class="k">Zadnji godišnjak</div><div class="v">${esc(d.godisnjak_zadnji||godisnjaci[godisnjaci.length-1])}</div>
|
<div class="k">Zadnji godišnjak</div><div class="v">${esc(d.godisnjak_zadnji||godisnjaci[godisnjaci.length-1])}</div>
|
||||||
<div class="k">Sve godine</div><div class="v">${godisnjaci.map(g=>'<span class="tag b">'+esc(g)+'</span>').join(' ')}</div>
|
<div class="k">Sve godine</div><div class="v">${godisnjaci.map(g=>'<span class="tag b">'+esc(g)+'</span>').join(' ')}</div>
|
||||||
</div>` : '<div class="empty">Nema podataka o godišnjacima</div>'}
|
</div>` : ''}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="p-nag" class="ptab" style="display:none">
|
${nagrade.length ? `<div class="pp-section-h" style="margin-top:18px">🏅 Nagrade <span class="cnt">${nagrade.length}</span></div>
|
||||||
${nagrade.length ? `<div style="overflow-x:auto"><table>
|
<div style="overflow-x:auto"><table>
|
||||||
<thead><tr><th>Godina</th><th>Nagrada</th><th>Razina</th></tr></thead>
|
<thead><tr><th>Godina</th><th>Nagrada</th><th>Razina</th></tr></thead>
|
||||||
<tbody>${nagrade.map(n => `
|
<tbody>${nagrade.map(n => `
|
||||||
<tr class="no-click">
|
<tr class="no-click">
|
||||||
@@ -2147,7 +2190,7 @@ async function openSportas(id){
|
|||||||
<td>${esc(n.razina||'')}</td>
|
<td>${esc(n.razina||'')}</td>
|
||||||
</tr>`).join('')}
|
</tr>`).join('')}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table></div>` : '<div class="empty">Nema zabilježenih nagrada</div>'}
|
</table></div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${enrichBlock('sportas', d.id)}
|
${enrichBlock('sportas', d.id)}
|
||||||
|
|||||||
Reference in New Issue
Block a user